diff --git a/package-lock.json b/package-lock.json index b438f20b..ddddd980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "pg-format": "^1.0.4", "pgsql-parser": "^13.1.11", "pino": "^7.6.3", + "postgres-array": "^3.0.1", "prettier": "^2.4.1", "prettier-plugin-sql": "^0.4.0", "sql-formatter": "^4.0.2" @@ -7688,6 +7689,14 @@ "node": ">=4" } }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, "node_modules/pgpass": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", @@ -8170,11 +8179,11 @@ } }, "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.1.tgz", + "integrity": "sha512-h7i53Dw2Yq3a1uuZ6lbVFAkvMMwssJ8jkzeAg0XaZm1XIFF/t/s+tockdqbWTymyEm07dVenOQbFisEi+kj8uA==", "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/postgres-bytea": { @@ -17227,6 +17236,13 @@ "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" + }, + "dependencies": { + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + } } }, "pgpass": { @@ -17595,9 +17611,9 @@ "dev": true }, "postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.1.tgz", + "integrity": "sha512-h7i53Dw2Yq3a1uuZ6lbVFAkvMMwssJ8jkzeAg0XaZm1XIFF/t/s+tockdqbWTymyEm07dVenOQbFisEi+kj8uA==" }, "postgres-bytea": { "version": "1.0.0", diff --git a/package.json b/package.json index 2f702431..ad0267d8 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,13 @@ "build:server": "tsc -p tsconfig.server.json && cpy 'src/lib/sql/*.sql' bin/src/lib/sql", "docs:export": "PG_META_EXPORT_DOCS=true ts-node-dev src/server/app.ts", "start": "NODE_ENV=production node bin/src/server/app.js", - "dev": "run-s db:clean db:run && NODE_ENV=development ts-node-dev src/server/app.ts | pino-pretty --colorize", + "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && NODE_ENV=development ts-node-dev src/server/app.ts | pino-pretty --colorize", "pkg": "run-s clean build:server && pkg --out-path bin .pkg.config.json", "test": "run-s db:clean db:run test:run db:clean", "db:clean": "cd test/db && docker-compose down", "db:run": "cd test/db && docker-compose up --detach && sleep 5", - "test:run": "jest --runInBand" + "test:run": "jest --runInBand", + "test:update": "run-s db:clean db:run && jest --runInBand --updateSnapshot && run-s db:clean" }, "engines": { "node": ">=14 <15", @@ -38,6 +39,7 @@ "pg-format": "^1.0.4", "pgsql-parser": "^13.1.11", "pino": "^7.6.3", + "postgres-array": "^3.0.1", "prettier": "^2.4.1", "prettier-plugin-sql": "^0.4.0", "sql-formatter": "^4.0.2" diff --git a/src/lib/PostgresMeta.ts b/src/lib/PostgresMeta.ts index 069cf21d..5084a820 100644 --- a/src/lib/PostgresMeta.ts +++ b/src/lib/PostgresMeta.ts @@ -14,9 +14,8 @@ import PostgresMetaTypes from './PostgresMetaTypes' import PostgresMetaVersion from './PostgresMetaVersion' import PostgresMetaViews from './PostgresMetaViews' import { init } from './db' -import { PostgresMetaResult } from './types' export default class PostgresMeta { - query: (sql: string) => Promise> + query: (sql: string) => Promise end: () => Promise columns: PostgresMetaColumns config: PostgresMetaConfig @@ -37,21 +36,21 @@ export default class PostgresMeta { format = Parser.Format constructor(config: PoolConfig) { - const { query, end } = init(config) - this.query = query + const { query, queryArrayMode, end } = init(config) + this.query = queryArrayMode this.end = end - this.columns = new PostgresMetaColumns(this.query) - this.config = new PostgresMetaConfig(this.query) - this.extensions = new PostgresMetaExtensions(this.query) - this.functions = new PostgresMetaFunctions(this.query) - this.policies = new PostgresMetaPolicies(this.query) - this.publications = new PostgresMetaPublications(this.query) - this.roles = new PostgresMetaRoles(this.query) - this.schemas = new PostgresMetaSchemas(this.query) - this.tables = new PostgresMetaTables(this.query) - this.triggers = new PostgresMetaTriggers(this.query) - this.types = new PostgresMetaTypes(this.query) - this.version = new PostgresMetaVersion(this.query) - this.views = new PostgresMetaViews(this.query) + this.columns = new PostgresMetaColumns(query) + this.config = new PostgresMetaConfig(query) + this.extensions = new PostgresMetaExtensions(query) + this.functions = new PostgresMetaFunctions(query) + this.policies = new PostgresMetaPolicies(query) + this.publications = new PostgresMetaPublications(query) + this.roles = new PostgresMetaRoles(query) + this.schemas = new PostgresMetaSchemas(query) + this.tables = new PostgresMetaTables(query) + this.triggers = new PostgresMetaTriggers(query) + this.types = new PostgresMetaTypes(query) + this.version = new PostgresMetaVersion(query) + this.views = new PostgresMetaViews(query) } } diff --git a/src/lib/db.ts b/src/lib/db.ts index 6924df90..dd6718ca 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,10 +1,18 @@ import { types, Pool, PoolConfig } from 'pg' +import { parse as parseArray } from 'postgres-array' import { PostgresMetaResult } from './types' -types.setTypeParser(20, parseInt) +types.setTypeParser(types.builtins.INT8, parseInt) +types.setTypeParser(types.builtins.DATE, (x) => x) +types.setTypeParser(types.builtins.TIMESTAMP, (x) => x) +types.setTypeParser(types.builtins.TIMESTAMPTZ, (x) => x) +types.setTypeParser(1115, parseArray) // _timestamp +types.setTypeParser(1182, parseArray) // _date +types.setTypeParser(1185, parseArray) // _timestamptz export const init: (config: PoolConfig) => { query: (sql: string) => Promise> + queryArrayMode: (sql: string) => Promise end: () => Promise } = (config) => { // NOTE: Race condition could happen here: one async task may be doing @@ -31,6 +39,45 @@ export const init: (config: PoolConfig) => { } }, + async queryArrayMode(sql) { + try { + if (!pool) { + const pool = new Pool(config) + let res: any = await pool.query({ + rowMode: 'array', + text: sql, + }) + if (!Array.isArray(res)) { + res = [res] + } + return { + data: res.map(({ fields, rows }: any) => ({ + columns: fields.map((x: any) => x.name), + rows, + })), + error: null, + } + } + + let res: any = await pool.query({ + rowMode: 'array', + text: sql, + }) + if (!Array.isArray(res)) { + res = [res] + } + return { + data: res.map(({ fields, rows }: any) => ({ + columns: fields.map((x: any) => x.name), + rows, + })), + error: null, + } + } catch (e: any) { + return { data: null, error: { message: e.message } } + } + }, + async end() { const _pool = pool pool = null diff --git a/test/lib/columns.ts b/test/lib/columns.ts index 31549093..35f2ac9c 100644 --- a/test/lib/columns.ts +++ b/test/lib/columns.ts @@ -235,7 +235,14 @@ AND i.indisprimary; Object { "data": Array [ Object { - "attname": "c", + "columns": Array [ + "attname", + ], + "rows": Array [ + Array [ + "c", + ], + ], }, ], "error": null, @@ -267,7 +274,14 @@ AND i.indisunique; Object { "data": Array [ Object { - "attname": "c", + "columns": Array [ + "attname", + ], + "rows": Array [ + Array [ + "c", + ], + ], }, ], "error": null, @@ -387,7 +401,14 @@ SELECT pg_get_constraintdef(( Object { "data": Array [ Object { - "pg_get_constraintdef": null, + "columns": Array [ + "pg_get_constraintdef", + ], + "rows": Array [ + Array [ + null, + ], + ], }, ], "error": null, diff --git a/test/lib/query.ts b/test/lib/query.ts index 7715c376..5f467929 100644 --- a/test/lib/query.ts +++ b/test/lib/query.ts @@ -6,14 +6,23 @@ test('query', async () => { Object { "data": Array [ Object { - "id": 1, - "name": "Joe Bloggs", - "status": "ACTIVE", - }, - Object { - "id": 2, - "name": "Jane Doe", - "status": "ACTIVE", + "columns": Array [ + "id", + "name", + "status", + ], + "rows": Array [ + Array [ + 1, + "Joe Bloggs", + "ACTIVE", + ], + Array [ + 2, + "Jane Doe", + "ACTIVE", + ], + ], }, ], "error": null, @@ -463,14 +472,14 @@ CREATE TABLE table_name ( const deparse = pgMeta.deparse(res.data!) expect(deparse.data).toMatchInlineSnapshot(` -"CREATE TABLE table_name ( - id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - inserted_at pg_catalog.timestamptz DEFAULT ( timezone('utc'::text, now()) ) NOT NULL, - updated_at pg_catalog.timestamptz DEFAULT ( timezone('utc'::text, now()) ) NOT NULL, - data jsonb, - name text -);" -`) + "CREATE TABLE table_name ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + inserted_at pg_catalog.timestamptz DEFAULT ( timezone('utc'::text, now()) ) NOT NULL, + updated_at pg_catalog.timestamptz DEFAULT ( timezone('utc'::text, now()) ) NOT NULL, + data jsonb, + name text + );" + `) }) test('formatter', async () => { diff --git a/test/lib/roles.ts b/test/lib/roles.ts index b0727b6a..c7a57de1 100644 --- a/test/lib/roles.ts +++ b/test/lib/roles.ts @@ -442,7 +442,7 @@ test('retrieve, create, update, delete', async () => { "is_superuser": true, "name": "r", "password": "********", - "valid_until": 2020-01-01T00:00:00.000Z, + "valid_until": "2020-01-01 00:00:00+00", }, "error": null, } @@ -467,7 +467,7 @@ test('retrieve, create, update, delete', async () => { "is_superuser": true, "name": "r", "password": "********", - "valid_until": 2020-01-01T00:00:00.000Z, + "valid_until": "2020-01-01 00:00:00+00", }, "error": null, } @@ -507,7 +507,7 @@ test('retrieve, create, update, delete', async () => { "is_superuser": true, "name": "rr", "password": "********", - "valid_until": 2020-01-01T00:00:00.000Z, + "valid_until": "2020-01-01 00:00:00+00", }, "error": null, } @@ -532,7 +532,7 @@ test('retrieve, create, update, delete', async () => { "is_superuser": true, "name": "rr", "password": "********", - "valid_until": 2020-01-01T00:00:00.000Z, + "valid_until": "2020-01-01 00:00:00+00", }, "error": null, }