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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This package provides an implementation for the `@aws-appsync/utils` package tha

## Changelog:

- v0.1.2: fix `rds` query builders with schema qualified identifiers
- v0.1.1: security updates
- v0.1.0: first pinned version of the library

Expand Down
64 changes: 64 additions & 0 deletions __tests__/__snapshots__/resolvers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,70 @@ exports[`rds resolvers postgresql update 1`] = `
}
`;

exports[`rds resolvers schema-qualified identifiers mysql select qualified table 1`] = `
{
"statements": [
"SELECT * FROM \`domain\`.\`item\`",
],
"variableMap": {},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers schema-qualified identifiers postgresql insert into qualified table 1`] = `
{
"statements": [
"INSERT INTO "private"."persons" ("name") VALUES (:P0)",
],
"variableMap": {
":P0": "test",
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers schema-qualified identifiers postgresql qualified column in where clause 1`] = `
{
"statements": [
"SELECT * FROM "private"."persons" WHERE "persons"."id" = :P0",
],
"variableMap": {
":P0": 123,
},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers schema-qualified identifiers postgresql select qualified columns 1`] = `
{
"statements": [
"SELECT "id", "persons"."name" FROM "private"."persons"",
],
"variableMap": {},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers schema-qualified identifiers postgresql select qualified table 1`] = `
{
"statements": [
"SELECT * FROM "domain"."item"",
],
"variableMap": {},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers schema-qualified identifiers unqualified name is quoted as one segment 1`] = `
{
"statements": [
"SELECT * FROM "item"",
],
"variableMap": {},
"variableTypeHintMap": {},
}
`;

exports[`rds resolvers toJsonObject 1`] = `
[
[
Expand Down
74 changes: 74 additions & 0 deletions __tests__/resolvers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,80 @@ describe("rds resolvers", () => {

});
});

// Schema/table-qualified identifiers (e.g. "schema.table" or "table.column") must be split on
// `.` and each segment quoted individually, matching AWS AppSync (e.g. `"schema"."table"`),
// rather than quoting the whole string as one literal identifier (`"schema.table"`).
describe("schema-qualified identifiers", () => {
test("postgresql select qualified table", async () => {
const code = `
export function request(ctx) {
return rds.createPgStatement(rds.select({ table: "domain.item" }));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});

test("postgresql select qualified columns", async () => {
const code = `
export function request(ctx) {
return rds.createPgStatement(rds.select({
table: "private.persons",
columns: ["id", "persons.name"],
}));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});

test("postgresql qualified column in where clause", async () => {
const code = `
export function request(ctx) {
return rds.createPgStatement(rds.select({
table: "private.persons",
where: { "persons.id": { eq: 123 } },
}));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});

test("postgresql insert into qualified table", async () => {
const code = `
export function request(ctx) {
return rds.createPgStatement(rds.insert({
table: "private.persons",
values: { name: "test" },
}));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});

test("mysql select qualified table", async () => {
const code = `
export function request(ctx) {
return rds.createMySQLStatement(rds.select({ table: "domain.item" }));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});

test("unqualified name is quoted as one segment", async () => {
const code = `
export function request(ctx) {
return rds.createPgStatement(rds.select({ table: "item" }));
}
export function response(ctx) {}
`;
await checkResolverValid(code, {}, "request");
});
});
});

describe("error handling", () => {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.0",
"version": "0.1.2",
"description": "Implementation of the AppSync utils helpers",
"type": "module",
"main": "index.js",
Expand Down
37 changes: 22 additions & 15 deletions rds/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class StatementBuilder {
let query;

if (columns) {
const columnNames = columns.map(name => `${this.quoteChar}${name}${this.quoteChar}`).join(', ');
const columnNames = columns.map(name => this.quoteIdentifier(name)).join(', ');
query = `SELECT ${columnNames} FROM ${tableName}`;
} else {
query = `SELECT * FROM ${tableName}`;
Expand All @@ -170,7 +170,7 @@ class StatementBuilder {
let orderByParts = [];
for (let { column, dir } of orderBy) {
dir = dir || "ASC";
orderByParts.push(`${this.quoteChar}${column}${this.quoteChar} ${dir}`);
orderByParts.push(`${this.quoteIdentifier(column)} ${dir}`);
}

query = `${query} ORDER BY ${orderByParts.join(', ')}`;
Expand Down Expand Up @@ -202,7 +202,7 @@ class StatementBuilder {
}

if (returning) {
const columnNames = returning.map(name => `${this.quoteChar}${name}${this.quoteChar}`).join(', ');
const columnNames = returning.map(name => this.quoteIdentifier(name)).join(', ');
query = `${query} RETURNING ${columnNames}`;
}

Expand All @@ -218,7 +218,7 @@ class StatementBuilder {
let columnTextItems = [];
let valuesTextItems = [];
for (const [columnName, value] of Object.entries(values)) {
columnTextItems.push(`${this.quoteChar}${columnName}${this.quoteChar}`);
columnTextItems.push(this.quoteIdentifier(columnName));
const placeholder = this.newVariable(value);
valuesTextItems.push(placeholder);
}
Expand All @@ -240,7 +240,7 @@ class StatementBuilder {
let columnDefinitionItems = [];
for (const [columnName, value] of Object.entries(values)) {
const placeholder = this.newVariable(value);
columnDefinitionItems.push(`${this.quoteChar}${columnName}${this.quoteChar} = ${placeholder}`);
columnDefinitionItems.push(`${this.quoteIdentifier(columnName)} = ${placeholder}`);

}
query = `${query} ${columnDefinitionItems.join(', ')}`;
Expand Down Expand Up @@ -310,30 +310,37 @@ class StatementBuilder {
}
switch (conditionType) {
case "eq":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} = ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} = ${value}${endGrouping}`;
case "ne":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} != ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} != ${value}${endGrouping}`;
case "gt":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} > ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} > ${value}${endGrouping}`;
case "lt":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} < ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} < ${value}${endGrouping}`;
case "ge":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} >= ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} >= ${value}${endGrouping}`;
case "le":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} <= ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} <= ${value}${endGrouping}`;
case "contains":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} LIKE ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} LIKE ${value}${endGrouping}`;
case "notContains":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} NOT LIKE ${value}${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} NOT LIKE ${value}${endGrouping}`;
case "attributeExists":
return `${startGrouping}${this.quoteChar}${columnName}${this.quoteChar} IS ${value? "NOT " : ""}NULL${endGrouping}`;
return `${startGrouping}${this.quoteIdentifier(columnName)} IS ${value? "NOT " : ""}NULL${endGrouping}`;
default:
throw new Error(`Unhandled condition type ${conditionType}`);
}
}

getTableName(rawName) {
return `${this.quoteChar}${rawName}${this.quoteChar}`;
return this.quoteIdentifier(rawName);
}

quoteIdentifier(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"`).
return rawName.split('.').map(part => `${this.quoteChar}${part}${this.quoteChar}`).join('.');
}
}

Expand Down
Loading