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
6 changes: 3 additions & 3 deletions .github/workflows/test-appsync-utils.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:

env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_API_KEY }}
TEST_IMAGE_NAME: public.ecr.aws/lambda/nodejs:16
TEST_IMAGE_NAME: public.ecr.aws/lambda/nodejs:18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er... we are still using 16 (for better or worse...) https://github.com/localstack/localstack-ext/blob/1f952f304e39d1d066a4d703a2727b395bf8af9e/localstack-pro-core/localstack/pro/core/services/appsync/mapping.py#L25

However we do want to move our JS execution to the node in the container image, so this is a good move, thanks! 👍


jobs:
unit-test:
Expand All @@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 18
- run: npm ci
- run: npm test

Expand All @@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 18

- name: Pull test docker image
run: docker pull $TEST_IMAGE_NAME
Expand Down
93 changes: 93 additions & 0 deletions __tests__/__snapshots__/resolvers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ exports[`dynamodb resolvers something 2`] = `
]
`;

exports[`rds resolvers attributeExists nested or 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE "id" = :P0 AND (("deleted" = :P1) OR ("deleted" IS NULL))",
],
"variableMap": {
":P0": "123456",
":P1": false,
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers attributeExists true false 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE "created" IS NOT NULL AND "deleted" IS NULL",
],
"variableMap": {},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers mysql insert 1`] = `
{
"statements": [
Expand Down Expand Up @@ -362,3 +385,73 @@ exports[`rds resolvers typehints UUID 1`] = `
"value": "abc123",
}
`;

exports[`rds resolvers where mixed inline and 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE "count" <= :P0 AND ("id" = :P1) AND "deleted" IS NULL",
],
"variableMap": {
":P0": 10,
":P1": 123456,
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers where mixed or/and 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE ("id" = :P0) AND ("id" = :P1) AND "id" = :P2",
],
"variableMap": {
":P0": "and eq",
":P1": "or eq 2",
":P2": "id eq",
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers where mixed or/and multiple or 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE ("id" = :P0) AND ("id" = :P1) OR ("id" = :P2) AND "id" = :P3",
],
"variableMap": {
":P0": "and eq",
":P1": "or eq 1",
":P2": "or eq 2",
":P3": "id eq",
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers where nested ors with ands 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE "id" = :P0 AND ("id" = :P1) OR (("id" = :P2) OR ("id" = :P3)) OR ("id" = :P4)",
],
"variableMap": {
":P0": "id eq",
":P1": "or 1",
":P2": "or nested 1",
":P3": "or nested 2",
":P4": "final or",
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers where single value in and 1`] = `
{
"statements": [
"SELECT * FROM "supplier" WHERE ("id" = :P0)",
],
"variableMap": {
":P0": 123456,
},
"variableTypeHintMap": {},
}
`;
147 changes: 144 additions & 3 deletions __tests__/resolvers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe("rds resolvers", () => {

const context = {
arguments: {
id: new Date(2023, 1, 1),
id: new Date(Date.UTC(2023, 1, 1)),
},
};

Expand Down Expand Up @@ -503,6 +503,147 @@ describe("rds resolvers", () => {
await checkResolverValid(code, responseContext, "response");
})

test("where mixed inline and", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
count: { le: 10 },
and: [{ id: { eq: 123456 } }],
deleted: { attributeExists: false }
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("where single value in and", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
and: [{ id: { eq: 123456 } }],
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("where mixed or/and", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
and: [{ id: { eq: "and eq" } }],
or: [{ id: { eq: "or eq 2" } }],
id: { eq: "id eq" },
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("where mixed or/and multiple or", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
and: [{ id: { eq: "and eq" } }],
or: [
{ id: { eq: "or eq 1" } },
{ id: { eq: "or eq 2" } }
],
id: { eq: "id eq" },
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("where nested ors with ands", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
id: { eq: "id eq" },
or: [
{ id: { eq: "or 1" } },
{
or: [
{ id: { eq: "or nested 1" } },
{ id: { eq: "or nested 2" } }
]
},
{ id: { eq: "final or" } }
]
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("attributeExists true false", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
created: { attributeExists: true },
deleted: { attributeExists: false }
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})

test("attributeExists nested or", async () => {
const code = `
export function request(ctx) {
const query = rds.select({
table: 'supplier',
where: {
id: {
eq: "123456"
},
and: [{
or: [
{ deleted: { eq: false } },
{ deleted: { attributeExists: false } }
]
}
]
}
});
return rds.createPgStatement(query);
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
})


describe("mysql", () => {
test("raw string", async () => {
Expand Down Expand Up @@ -566,7 +707,7 @@ describe("rds resolvers", () => {
arguments: {
id: "1232",
name: "hello",
started: new Date(2022, 2, 2),
started: new Date(Date.UTC(2022, 2, 2)),
}
};

Expand Down Expand Up @@ -757,7 +898,7 @@ describe("rds resolvers", () => {
arguments: {
id: "1232",
name: "hello",
started: new Date(2022, 2, 2),
started: new Date(Date.UTC(2022, 2, 2)),
}
};

Expand Down
43 changes: 26 additions & 17 deletions rds/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,34 +273,41 @@ class StatementBuilder {
return name;
}

buildWhereClause(where) {
buildWhereClause(where, startGrouping = "", endGrouping = "", default_operator="AND") {
let blocks = [];
if (where.or) {
const parts = [];
for (const part of where.or) {
parts.push(this.buildWhereStatement(part));
}
blocks.push(parts.join(" OR "));
} else if (where.and) {
const parts = [];
for (const part of where.and) {
parts.push(this.buildWhereStatement(part));
for (const key in where) {
if ( ["or", "and"].includes(key)) {
const ops = key.toUpperCase();
if (!Array.isArray(where[key])) {
// TODO properly handle errors to return a more useful message
throw new Error(`'${key}' expects conditions to be an array`);
}
const parts = where[key].map(
part => this.buildWhereClause(part, "(", ")", ops)
);
blocks.push(`${startGrouping}${parts.join(` ${ops} `)}${endGrouping}`);
} else {
// implicit single clause
const block = {};
block[key] = where[key];
blocks.push(this.buildWhereStatement(block, startGrouping, endGrouping));
}
blocks.push(parts.join(" AND "));
} else {
// implicit single clause
blocks.push(this.buildWhereStatement(where, "", ""));
}

return blocks;
return blocks.join(` ${default_operator} `);
}

buildWhereStatement(defn, startGrouping = "(", endGrouping = ")") {
const columnName = Object.keys(defn)[0];
const condition = defn[columnName];

const conditionType = Object.keys(condition)[0];
const value = this.newVariable(condition[conditionType]);
let value;
if (conditionType !== "attributeExists") {
value = this.newVariable(condition[conditionType]);
} else {
value = condition[conditionType];
}
switch (conditionType) {
case "eq":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} = ${value}${endGrouping}`;
Expand All @@ -318,6 +325,8 @@ class StatementBuilder {
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} LIKE ${value}${endGrouping}`;
case "notContains":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} NOT LIKE ${value}${endGrouping}`;
case "attributeExists":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} IS ${value? "NOT " : ""}NULL${endGrouping}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a super fan of this ternary operator, but it's quite common in JS so I'm ok with it.

default:
throw new Error(`Unhandled condition type ${conditionType}`);
}
Expand Down
2 changes: 1 addition & 1 deletion test_in_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if [ -z ${TEST_IN_DOCKER_ENTRYPOINT:-} ]; then
--workdir /test \
--entrypoint bash \
-e TEST_IN_DOCKER_ENTRYPOINT=1 \
${TEST_IMAGE_NAME:-public.ecr.aws/lambda/nodejs:16} /test_in_docker.sh
${TEST_IMAGE_NAME:-public.ecr.aws/lambda/nodejs:18} /test_in_docker.sh
else
# entrypoint
echo Entrypoint
Expand Down