Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions __tests__/__snapshots__/resolvers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `
[
[
Expand Down
3 changes: 2 additions & 1 deletion __tests__/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
128 changes: 128 additions & 0 deletions __tests__/resolvers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
23 changes: 18 additions & 5 deletions rds/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ class StatementBuilder {

if (where) {
const parts = this.buildWhereClause(where);
query = `${query} WHERE ${parts}`;
if (parts) {
query = `${query} WHERE ${parts}`;
}
}


Expand All @@ -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}`;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"`).
Expand Down
Loading