diff --git a/README.md b/README.md index 79dc290..147359c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__tests__/__snapshots__/resolvers.test.js.snap b/__tests__/__snapshots__/resolvers.test.js.snap index 4982c6f..9572cc5 100644 --- a/__tests__/__snapshots__/resolvers.test.js.snap +++ b/__tests__/__snapshots__/resolvers.test.js.snap @@ -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`] = ` [ [ diff --git a/__tests__/resolvers.test.js b/__tests__/resolvers.test.js index ee5e725..ee2133a 100644 --- a/__tests__/resolvers.test.js +++ b/__tests__/resolvers.test.js @@ -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", () => { diff --git a/package-lock.json b/package-lock.json index 15b4a54..7ad4317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@localstack/appsync-utils", - "version": "0.1.0", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@localstack/appsync-utils", - "version": "0.1.0", + "version": "0.1.2", "license": "Apache-2.0", "dependencies": { "uuid": "^14.0.1" diff --git a/package.json b/package.json index 24300c3..ca28e3b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/rds/index.js b/rds/index.js index 1c943cc..15c91ca 100644 --- a/rds/index.js +++ b/rds/index.js @@ -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}`; @@ -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(', ')}`; @@ -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}`; } @@ -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); } @@ -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(', ')}`; @@ -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('.'); } }