From 8ae7210e7096af08884014c56312fea7d127a2db Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Wed, 30 Oct 2019 02:30:31 -0400 Subject: [PATCH] Add config driven presets for insert, update and upserta --- psql/mutate.go | 33 +++++++++++++++++--- psql/mutate_test.go | 74 ++++++++++++++++++++++++++++++++++++++------- psql/psql_test.go | 8 +++++ 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/psql/mutate.go b/psql/mutate.go index bf15004..db5111f 100644 --- a/psql/mutate.go +++ b/psql/mutate.go @@ -82,11 +82,11 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, io.WriteString(c.w, `}}::json AS j) INSERT INTO `) quoted(c.w, ti.Name) io.WriteString(c.w, ` (`) - c.renderInsertUpdateColumns(qc, w, jt, ti) + c.renderInsertUpdateColumns(qc, w, jt, ti, false) io.WriteString(c.w, `)`) io.WriteString(c.w, ` SELECT `) - c.renderInsertUpdateColumns(qc, w, jt, ti) + c.renderInsertUpdateColumns(qc, w, jt, ti, true) io.WriteString(c.w, ` FROM input i, `) if array { @@ -107,7 +107,7 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, } func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer, - jt map[string]interface{}, ti *DBTableInfo) (uint32, error) { + jt map[string]interface{}, ti *DBTableInfo, values bool) (uint32, error) { root := &qc.Selects[0] i := 0 @@ -115,6 +115,9 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer if _, ok := jt[cn]; !ok { continue } + if _, ok := root.PresetMap[cn]; ok { + continue + } if len(root.Allowed) != 0 { if _, ok := root.Allowed[cn]; !ok { continue @@ -123,10 +126,30 @@ func (c *compilerContext) renderInsertUpdateColumns(qc *qcode.QCode, w io.Writer if i != 0 { io.WriteString(c.w, `, `) } + io.WriteString(c.w, `"`) io.WriteString(c.w, cn) + io.WriteString(c.w, `"`) i++ } + if i != 0 && len(root.PresetList) != 0 { + io.WriteString(c.w, `, `) + } + + for i := range root.PresetList { + if i != 0 { + io.WriteString(c.w, `, `) + } + if values { + io.WriteString(c.w, `'`) + io.WriteString(c.w, root.PresetMap[root.PresetList[i]]) + io.WriteString(c.w, `'`) + } else { + io.WriteString(c.w, `"`) + io.WriteString(c.w, root.PresetList[i]) + io.WriteString(c.w, `"`) + } + } return 0, nil } @@ -149,10 +172,10 @@ func (c *compilerContext) renderUpdate(qc *qcode.QCode, w io.Writer, io.WriteString(c.w, `}}::json AS j) UPDATE `) quoted(c.w, ti.Name) io.WriteString(c.w, ` SET (`) - c.renderInsertUpdateColumns(qc, w, jt, ti) + c.renderInsertUpdateColumns(qc, w, jt, ti, false) io.WriteString(c.w, `) = (SELECT `) - c.renderInsertUpdateColumns(qc, w, jt, ti) + c.renderInsertUpdateColumns(qc, w, jt, ti, true) io.WriteString(c.w, ` FROM input i, `) if array { diff --git a/psql/mutate_test.go b/psql/mutate_test.go index 29bf153..a6eaf86 100644 --- a/psql/mutate_test.go +++ b/psql/mutate_test.go @@ -2,6 +2,7 @@ package psql import ( "encoding/json" + "fmt" "testing" ) @@ -12,7 +13,7 @@ func simpleInsert(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" (full_name, email) SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -36,13 +37,13 @@ func singleInsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" (name, description, user_id) SELECT name, description, user_id FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description", "user_id") SELECT "name", "description", "user_id" FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` vars := map[string]json.RawMessage{ "insert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc", "user_id": 5 }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") + resSQL, err := compileGQLToPSQL(gql, vars, "anon") if err != nil { t.Fatal(err) } @@ -60,13 +61,13 @@ func bulkInsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{insert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` vars := map[string]json.RawMessage{ "insert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") + resSQL, err := compileGQLToPSQL(gql, vars, "anon") if err != nil { t.Fatal(err) } @@ -84,7 +85,7 @@ func singleUpsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` vars := map[string]json.RawMessage{ "upsert": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), @@ -108,7 +109,7 @@ func bulkUpsert(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" (name, description) SELECT name, description FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{upsert}}::json AS j) INSERT INTO "products" ("name", "description") SELECT "name", "description" FROM input i, json_populate_recordset(NULL::products, i.j) t ON CONFLICT DO (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` vars := map[string]json.RawMessage{ "upsert": json.RawMessage(` [{ "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }]`), @@ -132,13 +133,13 @@ func singleUpdate(t *testing.T) { } }` - sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET (name, description) = (SELECT name, description FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) AND (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` + sql := `WITH "products" AS (WITH "input" AS (SELECT {{update}}::json AS j) UPDATE "products" SET ("name", "description") = (SELECT "name", "description" FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."id") = 1) AND (("products"."id") = 15) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id", "products"."name" FROM "products") AS "products_0") AS "done_1337"` vars := map[string]json.RawMessage{ "update": json.RawMessage(` { "name": "my_name", "woo": { "hoo": "goo" }, "description": "my_desc" }`), } - resSQL, err := compileGQLToPSQL(gql, vars, "user") + resSQL, err := compileGQLToPSQL(gql, vars, "anon") if err != nil { t.Fatal(err) } @@ -179,7 +180,7 @@ func blockedInsert(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" (full_name, email) SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" ("full_name", "email") SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -203,7 +204,7 @@ func blockedUpdate(t *testing.T) { } }` - sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET (full_name, email) = (SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users") AS "users_0") AS "done_1337"` + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET ("full_name", "email") = (SELECT "full_name", "email" FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users") AS "users_0") AS "done_1337"` vars := map[string]json.RawMessage{ "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), @@ -219,6 +220,54 @@ func blockedUpdate(t *testing.T) { } } +func simpleInsertWithPresets(t *testing.T) { + gql := `mutation { + product(insert: $data) { + id + } + }` + + sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "products" ("name", "price", "created_at", "updated_at", "user_id") SELECT "name", "price", 'now', 'now', '$user_id' FROM input i, json_populate_record(NULL::products, i.j) t RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{"name": "Tomato", "price": 5.76}`), + } + + resSQL, err := compileGQLToPSQL(gql, vars, "user") + if err != nil { + t.Fatal(err) + } + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + +func simpleUpdateWithPresets(t *testing.T) { + gql := `mutation { + product(update: $data) { + id + } + }` + + sql := `WITH "products" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "products" SET ("name", "price", "updated_at") = (SELECT "name", "price", 'now' FROM input i, json_populate_record(NULL::products, i.j) t) WHERE (("products"."user_id") = {{user_id}}) RETURNING *) SELECT json_object_agg('product', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "products"."id" FROM "products") AS "products_0") AS "done_1337"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{"name": "Apple", "price": 1.25}`), + } + + resSQL, err := compileGQLToPSQL(gql, vars, "user") + if err != nil { + t.Fatal(err) + } + + fmt.Println(string(resSQL)) + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + func TestCompileMutate(t *testing.T) { t.Run("simpleInsert", simpleInsert) t.Run("singleInsert", singleInsert) @@ -229,4 +278,7 @@ func TestCompileMutate(t *testing.T) { t.Run("delete", delete) t.Run("blockedInsert", blockedInsert) t.Run("blockedUpdate", blockedUpdate) + t.Run("simpleInsertWithPresets", simpleInsertWithPresets) + t.Run("simpleUpdateWithPresets", simpleUpdateWithPresets) + } diff --git a/psql/psql_test.go b/psql/psql_test.go index 62e7a22..1023850 100644 --- a/psql/psql_test.go +++ b/psql/psql_test.go @@ -36,8 +36,16 @@ func TestMain(m *testing.M) { "{ price: { lt: 8 } }", }, }, + Insert: qcode.InsertConfig{ + Presets: map[string]string{ + "user_id": "$user_id", + "created_at": "now", + "updated_at": "now", + }, + }, Update: qcode.UpdateConfig{ Filters: []string{"{ user_id: { eq: $user_id } }"}, + Presets: map[string]string{"updated_at": "now"}, }, Delete: qcode.DeleteConfig{ Filters: []string{