diff --git a/__tests__/__snapshots__/resolvers.test.js.snap b/__tests__/__snapshots__/resolvers.test.js.snap index 9572cc5..583227e 100644 --- a/__tests__/__snapshots__/resolvers.test.js.snap +++ b/__tests__/__snapshots__/resolvers.test.js.snap @@ -341,6 +341,117 @@ exports[`rds resolvers schema-qualified identifiers unqualified name is quoted a } `; +exports[`rds resolvers select statement edge cases empty and condition in where 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color" LIMIT :P0", + ], + "variableMap": { + ":P0": 3, + }, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases empty where object 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color" LIMIT :P0", + ], + "variableMap": { + ":P0": 3, + }, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases empty where object in remove 1`] = ` +{ + "statements": [ + "DELETE FROM "persons"", + ], + "variableMap": {}, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases empty where object in update 1`] = ` +{ + "statements": [ + "UPDATE "persons" SET "name" = :P0", + ], + "variableMap": { + ":P0": "test", + }, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases limit and offset absent from args 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color"", + ], + "variableMap": {}, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases limit and offset zero 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color" LIMIT :P0 OFFSET :P1", + ], + "variableMap": { + ":P0": 0, + ":P1": 0, + }, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases limit provided in args 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color" LIMIT :P0", + ], + "variableMap": { + ":P0": 3, + }, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases mysql star column 1`] = ` +{ + "statements": [ + "SELECT * FROM \`domain\`.\`color\`", + ], + "variableMap": {}, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases postgresql star column 1`] = ` +{ + "statements": [ + "SELECT * FROM "domain"."color"", + ], + "variableMap": {}, + "variableTypeHintMap": {}, +} +`; + +exports[`rds resolvers select statement edge cases qualified star column 1`] = ` +{ + "statements": [ + "SELECT "id", "persons"."*" FROM "private"."persons"", + ], + "variableMap": {}, + "variableTypeHintMap": {}, +} +`; + exports[`rds resolvers toJsonObject 1`] = ` [ [ diff --git a/__tests__/helpers.js b/__tests__/helpers.js index 092558a..eda13b2 100644 --- a/__tests__/helpers.js +++ b/__tests__/helpers.js @@ -95,7 +95,8 @@ export const checkResolverValid = async (code, context, functionName) => { // manipulate the context object to behave like the AppSync equivalent function transformContextForAppSync(context) { - context.args = context.arguments; + // AWS always exposes `ctx.args`, even when the context carries no arguments at all + context.args = context.arguments ?? {}; } // If TEST_TARGET is AWS_CLOUD then run the check against AWS. Otherwise, run locally. diff --git a/__tests__/resolvers.test.js b/__tests__/resolvers.test.js index ee2133a..60d97a3 100644 --- a/__tests__/resolvers.test.js +++ b/__tests__/resolvers.test.js @@ -1088,6 +1088,134 @@ describe("rds resolvers", () => { await checkResolverValid(code, {}, "request"); }); }); + + // A `*` column must stay unquoted (`SELECT *`, not `SELECT "*"`), limit/offset are optional, + // and an empty `where` object must not emit a dangling `WHERE` keyword. + describe("select statement edge cases", () => { + // resolver shape taken from a customer: limit/offset forwarded straight from ctx.args + const optionalLimitOffsetCode = ` + export function request(ctx) { + const { limit, offset = null } = ctx.args; + return rds.createPgStatement(rds.select({ + table: 'domain.color', + columns: ['*'], + limit, + offset, + })); + } + export function response(ctx) {} + `; + + test("limit and offset absent from args", async () => { + await checkResolverValid(optionalLimitOffsetCode, {}, "request"); + }); + + test("limit provided in args", async () => { + await checkResolverValid(optionalLimitOffsetCode, { arguments: { limit: 3 } }, "request"); + }); + + test("postgresql star column", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.select({ table: "domain.color", columns: ["*"] })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("mysql star column", async () => { + const code = ` + export function request(ctx) { + return rds.createMySQLStatement(rds.select({ table: "domain.color", columns: ["*"] })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("qualified star column", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.select({ + table: "private.persons", + columns: ["id", "persons.*"], + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("empty where object", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.select({ + table: "domain.color", + where: {}, + limit: 3, + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("empty and condition in where", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.select({ + table: "domain.color", + where: { and: [] }, + limit: 3, + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("empty where object in update", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.update({ + table: "persons", + values: { name: "test" }, + where: {}, + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("empty where object in remove", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.remove({ + table: "persons", + where: {}, + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + + test("limit and offset zero", async () => { + const code = ` + export function request(ctx) { + return rds.createPgStatement(rds.select({ + table: "domain.color", + limit: 0, + offset: 0, + })); + } + export function response(ctx) {} + `; + await checkResolverValid(code, {}, "request"); + }); + }); }); describe("error handling", () => { diff --git a/package.json b/package.json index a83e501..01b3962 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@localstack/appsync-utils", - "version": "0.1.2", + "version": "0.1.3", "description": "Implementation of the AppSync utils helpers", "type": "module", "main": "index.js", diff --git a/rds/index.js b/rds/index.js index 15c91ca..a8f5898 100644 --- a/rds/index.js +++ b/rds/index.js @@ -162,7 +162,9 @@ class StatementBuilder { if (where) { const parts = this.buildWhereClause(where); - query = `${query} WHERE ${parts}`; + if (parts) { + query = `${query} WHERE ${parts}`; + } } @@ -177,12 +179,13 @@ class StatementBuilder { }; - if (limit) { + // limit/offset are optional and may be passed as null; 0 is a valid value + if (limit != null) { const limitValue = this.newVariable(limit); query = `${query} LIMIT ${limitValue}`; } - if (offset) { + if (offset != null) { const offsetValue = this.newVariable(offset); query = `${query} OFFSET ${offsetValue}`; } @@ -198,7 +201,9 @@ class StatementBuilder { if (where) { const parts = this.buildWhereClause(where); - query = `${query} WHERE ${parts}`; + if (parts) { + query = `${query} WHERE ${parts}`; + } } if (returning) { @@ -247,7 +252,9 @@ class StatementBuilder { if (where) { const parts = this.buildWhereClause(where); - query = `${query} WHERE ${parts}`; + if (parts) { + query = `${query} WHERE ${parts}`; + } } this.result.statements.push(query); @@ -337,6 +344,12 @@ class StatementBuilder { } quoteIdentifier(rawName) { + // A bare `*` stays unquoted (`SELECT *`), matching AWS AppSync. Note that AWS quotes the + // star in a qualified identifier (`persons.*` becomes `"persons"."*"`), so only the exact + // string `*` is special-cased. + if (rawName === '*') { + return rawName; + } // Split schema/table-qualified identifiers (e.g. "schema.table" or "table.column") on `.` // and quote each segment individually, matching AWS AppSync (e.g. `"schema"."table"`) // rather than quoting the whole string as one literal identifier (`"schema.table"`).