From f5a6782e17f5a1040ac217dc75bc0384864588d4 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Sat, 19 Mar 2022 15:01:39 +0100 Subject: [PATCH 1/7] feat: batch endpoints for column creation and retrieval --- src/lib/PostgresMetaColumns.ts | 173 +++++++++++++++++++++++---------- src/server/routes/columns.ts | 38 ++++++++ test/lib/columns.ts | 132 ++++++++++++++++++++++++- 3 files changed, 290 insertions(+), 53 deletions(-) diff --git a/src/lib/PostgresMetaColumns.ts b/src/lib/PostgresMetaColumns.ts index 01897c06..e506ff92 100644 --- a/src/lib/PostgresMetaColumns.ts +++ b/src/lib/PostgresMetaColumns.ts @@ -4,6 +4,28 @@ import { DEFAULT_SYSTEM_SCHEMAS } from './constants' import { columnsSql } from './sql' import { PostgresMetaResult, PostgresColumn } from './types' +interface ColumnCreationRequest { + table_id: number + name: string + type: string + default_value?: any + default_value_format?: 'expression' | 'literal' + is_identity?: boolean + identity_generation?: 'BY DEFAULT' | 'ALWAYS' + is_nullable?: boolean + is_primary_key?: boolean + is_unique?: boolean + comment?: string + check?: string +} + +interface ColumnBatchInfoRequest { + ids?: string[] + names?: string[] + table?: string + schema?: string +} + export default class PostgresMetaColumns { query: (sql: string) => Promise> metaTables: PostgresMetaTables @@ -57,75 +79,130 @@ export default class PostgresMetaColumns { schema?: string }): Promise> { if (id) { - const regexp = /^(\d+)\.(\d+)$/ - if (!regexp.test(id)) { - return { data: null, error: { message: 'Invalid format for column ID' } } + const { data, error } = await this.batchRetrieve({ ids: [id] }) + if (data) { + return { data: data[0], error: null } + } else if (error) { + return { data: null, error: error } + } + } + if (name && table) { + const { data, error } = await this.batchRetrieve({ names: [name], table, schema }) + if (data) { + return { data: data[0], error: null } + } else if (error) { + return { data: null, error: error } } - const matches = id.match(regexp) as RegExpMatchArray - const [tableId, ordinalPos] = matches.slice(1).map(Number) - const sql = `${columnsSql} AND c.oid = ${tableId} AND a.attnum = ${ordinalPos};` + } + return { data: null, error: { message: 'Invalid parameters on column retrieve' } } + } + + async batchRetrieve({ + ids, + names, + table, + schema = 'public', + }: ColumnBatchInfoRequest): Promise> { + if (ids && ids.length > 0) { + const regexp = /^(\d+)\.(\d+)$/ + const filteringClauses = ids + .map((id) => { + if (!regexp.test(id)) { + return { data: null, error: { message: 'Invalid format for column ID' } } + } + const matches = id.match(regexp) as RegExpMatchArray + const [tableId, ordinalPos] = matches.slice(1).map(Number) + return `(c.oid = ${tableId} AND a.attnum = ${ordinalPos})` + }) + .join(' OR ') + const sql = `${columnsSql} AND (${filteringClauses});` const { data, error } = await this.query(sql) if (error) { return { data, error } - } else if (data.length === 0) { - return { data: null, error: { message: `Cannot find a column with ID ${id}` } } + } else if (data.length < ids.length) { + return { data: null, error: { message: `Cannot find some of the requested columns.` } } } else { - return { data: data[0], error } + return { data, error } } - } else if (name && table) { - const sql = `${columnsSql} AND a.attname = ${literal(name)} AND c.relname = ${literal( + } else if (names && names.length > 0 && table) { + const filteringClauses = names.map((name) => `a.attname = ${literal(name)}`).join(' OR ') + const sql = `${columnsSql} AND (${filteringClauses}) AND c.relname = ${literal( table )} AND nc.nspname = ${literal(schema)};` const { data, error } = await this.query(sql) if (error) { return { data, error } - } else if (data.length === 0) { + } else if (data.length < names.length) { return { data: null, - error: { message: `Cannot find a column named ${name} in table ${schema}.${table}` }, + error: { message: `Cannot find some of the requested columns.` }, } } else { - return { data: data[0], error } + return { data, error } } } else { return { data: null, error: { message: 'Invalid parameters on column retrieve' } } } } - async create({ - table_id, - name, - type, - default_value, - default_value_format = 'literal', - is_identity = false, - identity_generation = 'BY DEFAULT', - // Can't pick a value as default since regular columns are nullable by default but PK columns aren't - is_nullable, - is_primary_key = false, - is_unique = false, - comment, - check, - }: { - table_id: number - name: string - type: string - default_value?: any - default_value_format?: 'expression' | 'literal' - is_identity?: boolean - identity_generation?: 'BY DEFAULT' | 'ALWAYS' - is_nullable?: boolean - is_primary_key?: boolean - is_unique?: boolean - comment?: string - check?: string - }): Promise> { + async create(col: ColumnCreationRequest): Promise> { + const { data, error } = await this.batchCreate([col]) + if (data) { + return { data: data[0], error: null } + } else if (error) { + return { data: null, error: error } + } + return { data: null, error: { message: 'Invalid params' } } + } + + async batchCreate(cols: ColumnCreationRequest[]): Promise> { + if (cols.length < 1) { + throw new Error('no columns provided for creation') + } + if ([...new Set(cols.map((col) => col.table_id))].length > 1) { + throw new Error('all columns in a single request must share the same table') + } + const { table_id } = cols[0] const { data, error } = await this.metaTables.retrieve({ id: table_id }) if (error) { return { data: null, error } } const { name: table, schema } = data! + const sqlStrings = cols.map((col) => this.generateColumnCreationSql(col, schema, table)) + + const sql = `BEGIN; +${sqlStrings.join('\n')} +COMMIT; +` + { + const { error } = await this.query(sql) + if (error) { + return { data: null, error } + } + } + const names = cols.map((col) => col.name) + return await this.batchRetrieve({ names, table, schema }) + } + + generateColumnCreationSql( + { + name, + type, + default_value, + default_value_format = 'literal', + is_identity = false, + identity_generation = 'BY DEFAULT', + // Can't pick a value as default since regular columns are nullable by default but PK columns aren't + is_nullable, + is_primary_key = false, + is_unique = false, + comment, + check, + }: ColumnCreationRequest, + schema: string, + table: string + ) { let defaultValueClause = '' if (is_identity) { if (default_value !== undefined) { @@ -159,22 +236,14 @@ export default class PostgresMetaColumns { : `COMMENT ON COLUMN ${ident(schema)}.${ident(table)}.${ident(name)} IS ${literal(comment)}` const sql = ` -BEGIN; ALTER TABLE ${ident(schema)}.${ident(table)} ADD COLUMN ${ident(name)} ${typeIdent(type)} ${defaultValueClause} ${isNullableClause} ${isPrimaryKeyClause} ${isUniqueClause} ${checkSql}; - ${commentSql}; -COMMIT;` - { - const { error } = await this.query(sql) - if (error) { - return { data: null, error } - } - } - return await this.retrieve({ name, table, schema }) + ${commentSql};` + return sql } async update( diff --git a/src/server/routes/columns.ts b/src/server/routes/columns.ts index 9451702b..5844e7dc 100644 --- a/src/server/routes/columns.ts +++ b/src/server/routes/columns.ts @@ -33,6 +33,7 @@ export default async (fastify: FastifyInstance) => { return data }) + // deprecated: use GET /batch instead fastify.get<{ Headers: { pg: string } Params: { @@ -54,6 +55,26 @@ export default async (fastify: FastifyInstance) => { return data }) + fastify.get<{ + Headers: { pg: string } + Body: any + }>('/batch', async (request, reply) => { + const connectionString = request.headers.pg + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.batchRetrieve({ ids: request.body }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + }) + + // deprecated: use POST /batch instead + // TODO (darora): specifying a schema on the routes would both allow for validation, and enable us to mark methods as deprecated fastify.post<{ Headers: { pg: string } Body: any @@ -69,7 +90,24 @@ export default async (fastify: FastifyInstance) => { if (error.message.startsWith('Cannot find')) reply.code(404) return { error: error.message } } + return data + }) + + fastify.post<{ + Headers: { pg: string } + Body: any + }>('/batch', async (request, reply) => { + const connectionString = request.headers.pg + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.batchCreate(request.body) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } return data }) diff --git a/test/lib/columns.ts b/test/lib/columns.ts index 00477274..60b076d0 100644 --- a/test/lib/columns.ts +++ b/test/lib/columns.ts @@ -170,13 +170,143 @@ test('retrieve, create, update, delete', async () => { expect(res).toMatchObject({ data: null, error: { - message: expect.stringMatching(/^Cannot find a column with ID \d+.1$/), + message: expect.stringMatching(/^Cannot find some of the requested columns.$/), }, }) await pgMeta.tables.remove(testTable!.id) }) +test('batch endpoints for create and retrieve', async () => { + const { data: testTable }: any = await pgMeta.tables.create({ name: 't' }) + + let res = await pgMeta.columns.batchCreate([ + { + table_id: testTable!.id, + name: 'c1', + type: 'int2', + default_value: 42, + comment: 'foo', + }, + { + table_id: testTable!.id, + name: 'c2', + type: 'int2', + default_value: 41, + comment: 'bar', + }, + ]) + expect(res).toMatchInlineSnapshot( + { + data: [ + { id: expect.stringMatching(/^\d+\.1$/), table_id: expect.any(Number) }, + { id: expect.stringMatching(/^\d+\.2$/), table_id: expect.any(Number) }, + ], + }, + ` + Object { + "data": Array [ + Object { + "comment": "foo", + "data_type": "smallint", + "default_value": "'42'::smallint", + "enums": Array [], + "format": "int2", + "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "c1", + "ordinal_position": 1, + "schema": "public", + "table": "t", + "table_id": Any, + }, + Object { + "comment": "bar", + "data_type": "smallint", + "default_value": "'41'::smallint", + "enums": Array [], + "format": "int2", + "id": StringMatching /\\^\\\\d\\+\\\\\\.2\\$/, + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "c2", + "ordinal_position": 2, + "schema": "public", + "table": "t", + "table_id": Any, + }, + ], + "error": null, + } + ` + ) + res = await pgMeta.columns.batchRetrieve({ ids: [res.data![0].id, res.data![1].id] }) + expect(res).toMatchInlineSnapshot( + { + data: [ + { id: expect.stringMatching(/^\d+\.1$/), table_id: expect.any(Number) }, + { id: expect.stringMatching(/^\d+\.2$/), table_id: expect.any(Number) }, + ], + }, + ` + Object { + "data": Array [ + Object { + "comment": "foo", + "data_type": "smallint", + "default_value": "'42'::smallint", + "enums": Array [], + "format": "int2", + "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "c1", + "ordinal_position": 1, + "schema": "public", + "table": "t", + "table_id": Any, + }, + Object { + "comment": "bar", + "data_type": "smallint", + "default_value": "'41'::smallint", + "enums": Array [], + "format": "int2", + "id": StringMatching /\\^\\\\d\\+\\\\\\.2\\$/, + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "c2", + "ordinal_position": 2, + "schema": "public", + "table": "t", + "table_id": Any, + }, + ], + "error": null, + } + ` + ) + + await pgMeta.tables.remove(testTable!.id) +}) + test('enum column with quoted name', async () => { await pgMeta.query('CREATE TYPE "T" AS ENUM (\'v\'); CREATE TABLE t ( c "T" );') From 6fd3b241b1346195273e38b5d95401150f931af5 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Sun, 20 Mar 2022 20:27:46 +0100 Subject: [PATCH 2/7] wip --- src/server/routes/columns.ts | 43 ++++++------------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/src/server/routes/columns.ts b/src/server/routes/columns.ts index 5844e7dc..df0af65c 100644 --- a/src/server/routes/columns.ts +++ b/src/server/routes/columns.ts @@ -55,24 +55,6 @@ export default async (fastify: FastifyInstance) => { return data }) - fastify.get<{ - Headers: { pg: string } - Body: any - }>('/batch', async (request, reply) => { - const connectionString = request.headers.pg - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.batchRetrieve({ ids: request.body }) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } - } - - return data - }) - // deprecated: use POST /batch instead // TODO (darora): specifying a schema on the routes would both allow for validation, and enable us to mark methods as deprecated fastify.post<{ @@ -80,26 +62,11 @@ export default async (fastify: FastifyInstance) => { Body: any }>('/', async (request, reply) => { const connectionString = request.headers.pg - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.create(request.body) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } + if (!Array.isArray(request.body)) { + request.body = [request.body] } - return data - }) - fastify.post<{ - Headers: { pg: string } - Body: any - }>('/batch', async (request, reply) => { - const connectionString = request.headers.pg - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) const { data, error } = await pgMeta.columns.batchCreate(request.body) await pgMeta.end() if (error) { @@ -108,7 +75,11 @@ export default async (fastify: FastifyInstance) => { if (error.message.startsWith('Cannot find')) reply.code(404) return { error: error.message } } - return data + + if (Array.isArray(request.body)) { + return data + } + return data[0] }) fastify.patch<{ From 69515d85f17ae844ad6b281ba14881762a80fa3e Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Sun, 20 Mar 2022 22:02:26 +0100 Subject: [PATCH 3/7] chore: use typebox for column create args type --- src/lib/PostgresMetaColumns.ts | 47 +++++++++++++++------------------- src/lib/types.ts | 20 +++++++++++++++ 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/lib/PostgresMetaColumns.ts b/src/lib/PostgresMetaColumns.ts index e506ff92..2cf3fdc8 100644 --- a/src/lib/PostgresMetaColumns.ts +++ b/src/lib/PostgresMetaColumns.ts @@ -2,29 +2,7 @@ import { ident, literal } from 'pg-format' import PostgresMetaTables from './PostgresMetaTables' import { DEFAULT_SYSTEM_SCHEMAS } from './constants' import { columnsSql } from './sql' -import { PostgresMetaResult, PostgresColumn } from './types' - -interface ColumnCreationRequest { - table_id: number - name: string - type: string - default_value?: any - default_value_format?: 'expression' | 'literal' - is_identity?: boolean - identity_generation?: 'BY DEFAULT' | 'ALWAYS' - is_nullable?: boolean - is_primary_key?: boolean - is_unique?: boolean - comment?: string - check?: string -} - -interface ColumnBatchInfoRequest { - ids?: string[] - names?: string[] - table?: string - schema?: string -} +import { PostgresMetaResult, PostgresColumn, PostgresColumnCreate } from './types' export default class PostgresMetaColumns { query: (sql: string) => Promise> @@ -97,12 +75,27 @@ export default class PostgresMetaColumns { return { data: null, error: { message: 'Invalid parameters on column retrieve' } } } + async batchRetrieve({ ids }: { ids: string[] }): Promise> + async batchRetrieve({ + names, + table, + schema, + }: { + names: string[] + table: string + schema: string + }): Promise> async batchRetrieve({ ids, names, table, schema = 'public', - }: ColumnBatchInfoRequest): Promise> { + }: { + ids?: string[] + names?: string[] + table?: string + schema?: string + }): Promise> { if (ids && ids.length > 0) { const regexp = /^(\d+)\.(\d+)$/ const filteringClauses = ids @@ -145,7 +138,7 @@ export default class PostgresMetaColumns { } } - async create(col: ColumnCreationRequest): Promise> { + async create(col: PostgresColumnCreate): Promise> { const { data, error } = await this.batchCreate([col]) if (data) { return { data: data[0], error: null } @@ -155,7 +148,7 @@ export default class PostgresMetaColumns { return { data: null, error: { message: 'Invalid params' } } } - async batchCreate(cols: ColumnCreationRequest[]): Promise> { + async batchCreate(cols: PostgresColumnCreate[]): Promise> { if (cols.length < 1) { throw new Error('no columns provided for creation') } @@ -199,7 +192,7 @@ COMMIT; is_unique = false, comment, check, - }: ColumnCreationRequest, + }: PostgresColumnCreate, schema: string, table: string ) { diff --git a/src/lib/types.ts b/src/lib/types.ts index 815eb796..1da4ca4a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -42,6 +42,26 @@ export const postgresColumnSchema = Type.Object({ }) export type PostgresColumn = Static +export const postgresColumnCreateSchema = Type.Object({ + table_id: Type.Integer(), + name: Type.String(), + type: Type.String(), + default_value: Type.Optional(Type.Any()), + default_value_format: Type.Optional( + Type.Union([Type.Literal('expression'), Type.Literal('literal')]) + ), + is_identity: Type.Optional(Type.Boolean()), + identity_generation: Type.Optional( + Type.Union([Type.Literal('BY DEFAULT'), Type.Literal('ALWAYS')]) + ), + is_nullable: Type.Optional(Type.Boolean()), + is_primary_key: Type.Optional(Type.Boolean()), + is_unique: Type.Optional(Type.Boolean()), + comment: Type.Optional(Type.String()), + check: Type.Optional(Type.String()), +}) +export type PostgresColumnCreate = Static + // TODO Rethink config.sql export const postgresConfigSchema = Type.Object({ name: Type.Unknown(), From cafcf99da15b87488fce5dcda7bd5dcea53afda3 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Sun, 20 Mar 2022 22:06:35 +0100 Subject: [PATCH 4/7] chore(server): validate batch column create --- src/server/routes/columns.ts | 68 ++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/server/routes/columns.ts b/src/server/routes/columns.ts index df0af65c..02ea2380 100644 --- a/src/server/routes/columns.ts +++ b/src/server/routes/columns.ts @@ -1,5 +1,11 @@ +import { Type } from '@sinclair/typebox' import { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../lib' +import { + PostgresColumnCreate, + postgresColumnSchema, + postgresColumnCreateSchema, +} from '../../lib/types' import { DEFAULT_POOL_CONFIG } from '../constants' import { extractRequestForLogging } from '../utils' @@ -33,7 +39,6 @@ export default async (fastify: FastifyInstance) => { return data }) - // deprecated: use GET /batch instead fastify.get<{ Headers: { pg: string } Params: { @@ -55,32 +60,51 @@ export default async (fastify: FastifyInstance) => { return data }) - // deprecated: use POST /batch instead - // TODO (darora): specifying a schema on the routes would both allow for validation, and enable us to mark methods as deprecated fastify.post<{ Headers: { pg: string } - Body: any - }>('/', async (request, reply) => { - const connectionString = request.headers.pg - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - if (!Array.isArray(request.body)) { - request.body = [request.body] - } + Body: PostgresColumnCreate | PostgresColumnCreate[] + }>( + '/', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + body: Type.Union([postgresColumnCreateSchema, Type.Array(postgresColumnCreateSchema)]), + response: { + 200: Type.Union([postgresColumnSchema, Type.Array(postgresColumnSchema)]), + 400: Type.Object({ + error: Type.String(), + }), + 404: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg - const { data, error } = await pgMeta.columns.batchCreate(request.body) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } - } + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + if (!Array.isArray(request.body)) { + request.body = [request.body] + } + + const { data, error } = await pgMeta.columns.batchCreate(request.body) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } - if (Array.isArray(request.body)) { - return data + if (Array.isArray(request.body)) { + return data + } + return data[0] } - return data[0] - }) + ) fastify.patch<{ Headers: { pg: string } From dc86a6ea5d9007291249840c00b8a512d85d5af0 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Sun, 20 Mar 2022 22:18:10 +0100 Subject: [PATCH 5/7] fix(lib/columns): error handling --- src/lib/PostgresMetaColumns.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/PostgresMetaColumns.ts b/src/lib/PostgresMetaColumns.ts index 2cf3fdc8..7a81d2a5 100644 --- a/src/lib/PostgresMetaColumns.ts +++ b/src/lib/PostgresMetaColumns.ts @@ -98,11 +98,14 @@ export default class PostgresMetaColumns { }): Promise> { if (ids && ids.length > 0) { const regexp = /^(\d+)\.(\d+)$/ + + const invalidId = ids.find((id) => !regexp.test(id)) + if (invalidId) { + return { data: null, error: { message: `Invalid format for column ID: ${invalidId}` } } + } + const filteringClauses = ids .map((id) => { - if (!regexp.test(id)) { - return { data: null, error: { message: 'Invalid format for column ID' } } - } const matches = id.match(regexp) as RegExpMatchArray const [tableId, ordinalPos] = matches.slice(1).map(Number) return `(c.oid = ${tableId} AND a.attnum = ${ordinalPos})` From 358f683fc31e4a720b1c69444a549455d8c0d282 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Mon, 21 Mar 2022 10:01:17 +0100 Subject: [PATCH 6/7] chore(lib/columns): show error for all invalid cols --- src/lib/PostgresMetaColumns.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/PostgresMetaColumns.ts b/src/lib/PostgresMetaColumns.ts index 7a81d2a5..468f14c9 100644 --- a/src/lib/PostgresMetaColumns.ts +++ b/src/lib/PostgresMetaColumns.ts @@ -99,9 +99,12 @@ export default class PostgresMetaColumns { if (ids && ids.length > 0) { const regexp = /^(\d+)\.(\d+)$/ - const invalidId = ids.find((id) => !regexp.test(id)) - if (invalidId) { - return { data: null, error: { message: `Invalid format for column ID: ${invalidId}` } } + const invalidIds = ids.filter((id) => !regexp.test(id)) + if (invalidIds.length > 0) { + return { + data: null, + error: { message: `Invalid format for column IDs: ${invalidIds.join(', ')}` }, + } } const filteringClauses = ids From fe11fbf82d5c3988b8334d50f12237dad9258c49 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Mon, 21 Mar 2022 17:10:36 +0100 Subject: [PATCH 7/7] fix: don't assign to request.body Makes the Array.isArray check at the end always true --- src/server/routes/columns.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/server/routes/columns.ts b/src/server/routes/columns.ts index 02ea2380..b12b9d97 100644 --- a/src/server/routes/columns.ts +++ b/src/server/routes/columns.ts @@ -84,13 +84,15 @@ export default async (fastify: FastifyInstance) => { }, async (request, reply) => { const connectionString = request.headers.pg - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - if (!Array.isArray(request.body)) { - request.body = [request.body] + let batchCreateArg: PostgresColumnCreate[] + if (Array.isArray(request.body)) { + batchCreateArg = request.body + } else { + batchCreateArg = [request.body] } - const { data, error } = await pgMeta.columns.batchCreate(request.body) + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.batchCreate(batchCreateArg) await pgMeta.end() if (error) { request.log.error({ error, request: extractRequestForLogging(request) })