diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 825c5fe69..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true, - "jest": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "overrides": [], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "simple-import-sort", - "unused-imports" - ], - "rules": { - "indent": [ - "error", - 2 - ], - "quotes": [ - "error", - "single", - { - "avoidEscape": true, - "allowTemplateLiterals": true - } - ], - "quote-props": [ - "error", - "as-needed" - ], - "semi": [ - "error", - "always" - ], - "comma-dangle": [ - "error", - "never" - ], - "simple-import-sort/imports": 1, - "simple-import-sort/exports": 1, - "unused-imports/no-unused-imports": 1, - "@typescript-eslint/no-unused-vars": [ - 1, - { - "argsIgnorePattern": "React|res|next|^_" - } - ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "no-console": 0, - "@typescript-eslint/ban-ts-comment": 0, - "prefer-const": 0, - "no-case-declarations": 0, - "no-implicit-globals": 0, - "@typescript-eslint/no-unsafe-declaration-merging": 0 - } -} diff --git a/__fixtures__/kitchen-sink/typescript.ts b/__fixtures__/kitchen-sink/typescript.ts index e3e62816c..80248f3a9 100644 --- a/__fixtures__/kitchen-sink/typescript.ts +++ b/__fixtures__/kitchen-sink/typescript.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { skitch } from './src/cli'; -var argv = require('minimist')(process.argv.slice(2)); +const argv = require('minimist')(process.argv.slice(2)); // skitch upgrade (async () => { diff --git a/__fixtures__/kitchen-sink/typescript.tsx b/__fixtures__/kitchen-sink/typescript.tsx index e3e62816c..80248f3a9 100644 --- a/__fixtures__/kitchen-sink/typescript.tsx +++ b/__fixtures__/kitchen-sink/typescript.tsx @@ -1,6 +1,6 @@ #!/usr/bin/env node import { skitch } from './src/cli'; -var argv = require('minimist')(process.argv.slice(2)); +const argv = require('minimist')(process.argv.slice(2)); // skitch upgrade (async () => { diff --git a/__fixtures__/output/schemas/actions_public.ts b/__fixtures__/output/schemas/actions_public.ts index 4f96634c6..2ed706091 100644 --- a/__fixtures__/output/schemas/actions_public.ts +++ b/__fixtures__/output/schemas/actions_public.ts @@ -1,4 +1,4 @@ -import { UUID, Timestamp } from "./_common"; +import { Timestamp,UUID } from "./_common"; export interface ast_actions { id: number; database_id: UUID; diff --git a/__fixtures__/output/schemas/collections_public.ts b/__fixtures__/output/schemas/collections_public.ts index 9d18118e2..bbfa44718 100644 --- a/__fixtures__/output/schemas/collections_public.ts +++ b/__fixtures__/output/schemas/collections_public.ts @@ -1,4 +1,4 @@ -import { UUID, Timestamp } from "./_common"; +import { Timestamp,UUID } from "./_common"; export interface check_constraint { id: UUID; database_id: UUID; diff --git a/__fixtures__/output/schemas/db_migrate.ts b/__fixtures__/output/schemas/db_migrate.ts index 4dbc778e1..967e3d0c6 100644 --- a/__fixtures__/output/schemas/db_migrate.ts +++ b/__fixtures__/output/schemas/db_migrate.ts @@ -1,4 +1,4 @@ -import { UUID, Timestamp } from "./_common"; +import { Timestamp,UUID } from "./_common"; export interface migrate_files { id: UUID; database_id: UUID | null; diff --git a/__fixtures__/output/schemas/objects_private.ts b/__fixtures__/output/schemas/objects_private.ts index 139cfb315..fba8b2b4c 100644 --- a/__fixtures__/output/schemas/objects_private.ts +++ b/__fixtures__/output/schemas/objects_private.ts @@ -1,4 +1,4 @@ -import { UUID, Timestamp } from "./_common"; +import { Timestamp,UUID } from "./_common"; export interface object { id: UUID; database_id: UUID; diff --git a/__fixtures__/output/schemas/txs_public.ts b/__fixtures__/output/schemas/txs_public.ts index 4f7ae4910..8db70fba9 100644 --- a/__fixtures__/output/schemas/txs_public.ts +++ b/__fixtures__/output/schemas/txs_public.ts @@ -1,4 +1,4 @@ -import { UUID, Timestamp } from "./_common"; +import { Timestamp,UUID } from "./_common"; export interface commit { id: UUID; message: string | null; diff --git a/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/achievements.test.ts b/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/achievements.test.ts index 6bc1b904f..afa1048cc 100644 --- a/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/achievements.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/achievements.test.ts @@ -65,6 +65,7 @@ it('newbie', async () => { 'accept_privacy', ...repeat('complete_action', 3) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -76,24 +77,28 @@ it('newbie', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -106,6 +111,7 @@ it('advanced', async () => { ...repeat('invite_users', 3), ...repeat('complete_action', 21) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -117,24 +123,28 @@ it('advanced', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -157,6 +167,7 @@ it('advanced part II', async () => { ...repeat('invite_users', 3), ...repeat('complete_action', 10) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -168,24 +179,28 @@ it('advanced part II', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -227,23 +242,27 @@ it('advanced part III', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); diff --git a/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/triggers.test.ts b/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/triggers.test.ts index 877408bad..81ba8f556 100644 --- a/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/triggers.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/achievements/__tests__/triggers.test.ts @@ -117,6 +117,7 @@ it('newbie', async () => { const beforeInsert = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ beforeInsert })).toMatchSnapshot(); await pg.any( @@ -127,6 +128,7 @@ it('newbie', async () => { const afterFirstInsert = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterFirstInsert })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET toggle = 'yo'`); @@ -134,6 +136,7 @@ it('newbie', async () => { const afterUpdateToggleToValue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterUpdateToggleToValue })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET toggle = NULL`); @@ -141,6 +144,7 @@ it('newbie', async () => { const afterUpdateToggleToNull = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterUpdateToggleToNull })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_verified = TRUE`); @@ -148,6 +152,7 @@ it('newbie', async () => { const afterIsVerifiedIsTrue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsVerifiedIsTrue })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_verified = FALSE`); @@ -155,6 +160,7 @@ it('newbie', async () => { const afterIsVerifiedIsFalse = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsVerifiedIsFalse })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_approved = TRUE`); @@ -162,5 +168,6 @@ it('newbie', async () => { const afterIsApprovedTrue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsApprovedTrue })).toMatchSnapshot(); }); diff --git a/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.decode.test.ts b/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.decode.test.ts index 13d30b244..b03c22a43 100644 --- a/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.decode.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.decode.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -18,6 +18,7 @@ it('base32_to_decimal', async () => { `SELECT base32.base32_to_decimal($1::text) AS base32_to_decimal`, ['INQXI==='] ); + expect(base32_to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']); }); @@ -26,6 +27,7 @@ it('decimal_to_chunks', async () => { `SELECT base32.decimal_to_chunks($1::text[]) AS decimal_to_chunks`, [['8', '13', '16', '23', '8', '=', '=', '=']] ); + expect(decimal_to_chunks).toEqual([ '01000', '01101', @@ -43,6 +45,7 @@ it('decode', async () => { `SELECT base32.decode($1::text) AS decode`, ['INQXI'] ); + expect(decode).toEqual('Cat'); }); @@ -51,6 +54,7 @@ it('zero_fill', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [300, 2] ); + expect(zero_fill).toBe('75'); }); @@ -59,6 +63,7 @@ it('zero_fill (-)', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [-300, 2] ); + expect(zero_fill).toBe('1073741749'); }); @@ -67,6 +72,7 @@ it('zero_fill (0)', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [-300, 0] ); + expect(zero_fill).toBe('4294966996'); }); @@ -77,6 +83,7 @@ cases( `SELECT base32.decode($1::text) AS decode`, [opts.name] ); + expect(decode).toEqual(opts.result); expect(decode).toMatchSnapshot(); }, diff --git a/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.encode.test.ts b/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.encode.test.ts index c807f5356..aaaabee41 100644 --- a/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.encode.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/base32/__tests__/base32.encode.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -18,6 +18,7 @@ it('to_ascii', async () => { `SELECT base32.to_ascii($1::text) AS to_ascii`, ['Cat'] ); + expect(to_ascii).toEqual([67, 97, 116]); }); @@ -30,6 +31,7 @@ it('to_binary', async () => { `SELECT base32.to_binary($1::int[]) AS to_binary`, [to_ascii] ); + expect(to_binary).toEqual(['01000011', '01100001', '01110100']); }); @@ -38,6 +40,7 @@ it('to_groups', async () => { `SELECT base32.to_groups($1::text[]) AS to_groups`, [['01000011', '01100001', '01110100']] ); + expect(to_groups).toEqual([ '01000011', '01100001', @@ -52,6 +55,7 @@ it('to_chunks', async () => { `SELECT base32.to_chunks($1::text[]) AS to_chunks`, [['01000011', '01100001', '01110100', 'xxxxxxxx', 'xxxxxxxx']] ); + expect(to_chunks).toEqual([ '01000', '01101', @@ -78,6 +82,7 @@ it('fill_chunks', async () => { 'xxxxx' ]] ); + expect(fill_chunks).toEqual([ '01000', '01101', @@ -104,6 +109,7 @@ it('to_decimal', async () => { 'xxxxx' ]] ); + expect(to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']); }); @@ -112,6 +118,7 @@ it('to_base32', async () => { `SELECT base32.to_base32($1::text[]) AS to_base32`, [['8', '13', '16', '23', '8', '=', '=', '=']] ); + expect(to_base32).toEqual('INQXI==='); }); @@ -122,6 +129,7 @@ cases( `SELECT base32.encode($1::text) AS encode`, [opts.name] ); + expect(encode).toEqual(opts.result); expect(encode).toMatchSnapshot(); }, diff --git a/__fixtures__/stage/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts b/__fixtures__/stage/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts index 453de4c04..f38f026cb 100644 --- a/__fixtures__/stage/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts @@ -30,6 +30,7 @@ describe('scheduled jobs', () => { } ] ); + objs.scheduled1 = result; }); @@ -48,6 +49,7 @@ describe('scheduled jobs', () => { { start, end, rule: '*/1 * * * *' } ] ); + objs.scheduled2 = result; }); @@ -58,6 +60,7 @@ describe('scheduled jobs', () => { ); const { queue_name, run_at, created_at, updated_at, ...obj } = result; + expect(obj).toMatchSnapshot(); }); diff --git a/__fixtures__/stage/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts b/__fixtures__/stage/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts index fc95af6cb..941cb8915 100644 --- a/__fixtures__/stage/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts @@ -78,6 +78,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -103,6 +104,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -128,6 +130,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -215,6 +218,7 @@ describe('db_meta_modules', () => { // Group by foreign table to see what they reference const foreignTables = [...new Set(fkConstraints.map(fk => fk.foreign_table_name))]; + expect(foreignTables).toContain('database'); expect(foreignTables).toContain('schema'); expect(foreignTables).toContain('table'); diff --git a/__fixtures__/stage/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts b/__fixtures__/stage/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts index 2ed050a3a..021b71690 100644 --- a/__fixtures__/stage/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts @@ -43,6 +43,7 @@ describe('db_meta functionality', () => { ...obj, dbname: 'test-database' // Replace dynamic dbname with static value }; + expect(snapshot(normalized)).toMatchSnapshot(); }; @@ -53,8 +54,10 @@ describe('db_meta functionality', () => { RETURNING *`, [owner_id, 'my-meta-db'] ); + objs.db = database; const database_id = database.id; + expect(snapshot(database)).toMatchSnapshot(); // Step 2: Create APIs first (since domains reference them) @@ -64,6 +67,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'public', 'authenticated', 'anonymous'] ); + objs.apis.public = publicApi; snapWithNormalizedDbname(publicApi); @@ -73,6 +77,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'admin', 'administrator', 'administrator'] ); + objs.apis.admin = adminApi; snapWithNormalizedDbname(adminApi); @@ -83,6 +88,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'Website Title', 'Website Description'] ); + objs.sites.app = appSite; snapWithNormalizedDbname(appSite); @@ -93,6 +99,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.apis.public.id, 'pgpm.io', 'api'] ); + objs.domains.api = apiDomain; expect(snapshot(apiDomain)).toMatchSnapshot(); @@ -102,6 +109,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.sites.app.id, 'pgpm.io', 'app'] ); + objs.domains.app = appDomain; expect(snapshot(appDomain)).toMatchSnapshot(); @@ -111,6 +119,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.apis.admin.id, 'pgpm.io', 'admin'] ); + objs.domains.admin = adminDomain; expect(snapshot(adminDomain)).toMatchSnapshot(); @@ -120,6 +129,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'pgpm.io'] ); + objs.domains.base = baseDomain; // Step 5: Register modules @@ -131,6 +141,7 @@ describe('db_meta functionality', () => { supportEmail: 'support@interweb.co' })] ); + expect(snapshot(siteModule1)).toMatchSnapshot(); const [apiModule] = await pg.any( @@ -142,6 +153,7 @@ describe('db_meta functionality', () => { authenticate: 'authenticate' })] ); + expect(snapshot(apiModule)).toMatchSnapshot(); const [siteModule2] = await pg.any( @@ -159,6 +171,7 @@ describe('db_meta functionality', () => { verify_email: 'verify_email' })] ); + expect(snapshot(siteModule2)).toMatchSnapshot(); // Step 6: Schema associations diff --git a/__fixtures__/stage/extensions/@pgpm/defaults/__tests__/defaults.test.ts b/__fixtures__/stage/extensions/@pgpm/defaults/__tests__/defaults.test.ts index 9bcdcf54f..45ced3549 100644 --- a/__fixtures__/stage/extensions/@pgpm/defaults/__tests__/defaults.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/defaults/__tests__/defaults.test.ts @@ -39,6 +39,7 @@ describe('defaults security configurations', () => { // If datacl exists, check that PUBLIC doesn't appear or has no privileges if (privileges[0].datacl) { const aclString = privileges[0].datacl.toString(); + // PUBLIC should not appear in the ACL with any privileges // The ACL format is {user=privileges/grantor,...} // PUBLIC would appear as "=privileges/grantor" or "PUBLIC=privileges/grantor" @@ -50,10 +51,12 @@ describe('defaults security configurations', () => { it('should verify database connection still works for current user', async () => { // Verify we can still connect and run queries const [result] = await pg.any(`SELECT 1 as test`); + expect(result.test).toBe(1); // Verify we can see current user const [userResult] = await pg.any(`SELECT current_user as username`); + expect(userResult.username).toBeDefined(); }); }); @@ -84,6 +87,7 @@ describe('defaults security configurations', () => { it('should allow current user to execute functions they create', async () => { // Current user should still be able to execute their own functions const [result] = await pg.any(`SELECT test_default_function() as result`); + expect(result.result).toBe('test result'); }); @@ -109,6 +113,7 @@ describe('defaults security configurations', () => { for (const priv of defaultPrivs) { if (priv.defaclacl) { const aclString = priv.defaclacl.toString(); + // Check that PUBLIC doesn't have execute (X) privileges // PUBLIC would appear as "=X/grantor" or "PUBLIC=X/grantor" expect(aclString).not.toMatch(/(?:^|,)(?:PUBLIC)?=[^,}]*X[^,}]*(?:,|$)/); @@ -177,6 +182,7 @@ describe('defaults security configurations', () => { const [result] = await pg.any(` SELECT secret_data FROM test_security_table LIMIT 1 `); + expect(result.secret_data).toBe('confidential information'); // Check table privileges for PUBLIC @@ -287,6 +293,7 @@ describe('defaults security configurations', () => { FROM pg_default_acl WHERE defaclobjtype = 'f' `); + // Should have some default ACL configuration expect(parseInt(defaultFuncPrivs[0].count)).toBeGreaterThanOrEqual(0); @@ -295,6 +302,7 @@ describe('defaults security configurations', () => { SELECT has_schema_privilege('public', 'public', 'create') as public_can_create_in_public `); + expect(schemaPrivs.public_can_create_in_public).toBe(false); }); diff --git a/__fixtures__/stage/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts b/__fixtures__/stage/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts index 197c08aab..fd989ad58 100644 --- a/__fixtures__/stage/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts @@ -27,6 +27,7 @@ describe('encrypted secrets table', () => { const schemas = await pg.any( `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'secrets_schema'` ); + expect(schemas).toHaveLength(1); expect(schemas[0].schema_name).toBe('secrets_schema'); }); @@ -128,6 +129,7 @@ describe('encrypted secrets table', () => { // Verify it's a bcrypt hash (starts with $2) const hashedValue = result.secrets_value_field.toString(); + expect(hashedValue).toMatch(/^\$2[aby]?\$/); }); @@ -191,6 +193,7 @@ describe('encrypted secrets table', () => { // Verify it's a bcrypt hash const hashedValue = updated.secrets_value_field.toString(); + expect(hashedValue).toMatch(/^\$2[aby]?\$/); }); @@ -234,6 +237,7 @@ describe('encrypted secrets table', () => { // The stored hash should be different from the original password const storedHash = result.secrets_value_field.toString(); + expect(storedHash).not.toBe(password); // Verify it's a proper bcrypt hash format @@ -252,6 +256,7 @@ describe('encrypted secrets table', () => { ); const storedHash2 = result2.secrets_value_field.toString(); + expect(storedHash2).not.toBe(storedHash); // Different salt = different hash expect(storedHash2).toMatch(/^\$2[aby]?\$/); }); diff --git a/__fixtures__/stage/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts b/__fixtures__/stage/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts index 3d3acaf27..9f7724588 100644 --- a/__fixtures__/stage/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts @@ -45,6 +45,7 @@ describe('encrypted secrets', () => { WHERE secrets_owned_field = $1`, [user_id] ); + expect(encrypt_field_pgp_get).toMatchSnapshot(); }); @@ -52,6 +53,7 @@ describe('encrypted secrets', () => { const [{ encrypt_field_set }] = await pg.any( `SELECT encrypted_secrets.encrypt_field_set('myvalue')` ); + expect(encrypt_field_set).toMatchSnapshot(); }); @@ -61,6 +63,7 @@ describe('encrypted secrets', () => { encrypted_secrets.encrypt_field_set('value-there-and-back') )` ); + expect(encrypt_field_bytea_to_text).toMatchSnapshot(); }); @@ -72,6 +75,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_getter).toMatchSnapshot(); }); @@ -84,6 +88,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_verify).toMatchSnapshot(); }); @@ -96,6 +101,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_upsert).toMatchSnapshot(); const [{ secrets_verify }] = await pg.any( @@ -106,6 +112,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_verify).toMatchSnapshot(); }); @@ -115,6 +122,7 @@ describe('encrypted secrets', () => { `SELECT encrypted_secrets.secrets_getter($1::uuid, 'my-secret-name')`, [user_id] ); + expect(beforeDelete.secrets_getter).toBe('my-secret'); // Delete the secret @@ -128,6 +136,7 @@ describe('encrypted secrets', () => { `SELECT encrypted_secrets.secrets_getter($1::uuid, 'my-secret-name', 'default-value')`, [user_id] ); + expect(afterDelete.secrets_getter).toBe('default-value'); }); @@ -171,6 +180,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(encrypt_field_pgp_getter).toMatchSnapshot(); }); @@ -188,6 +198,7 @@ describe('encrypted secrets', () => { }) ] ); + expect(secrets_table_upsert).toMatchSnapshot(); }); }); diff --git a/__fixtures__/stage/extensions/@pgpm/faker/__tests__/faker.test.ts b/__fixtures__/stage/extensions/@pgpm/faker/__tests__/faker.test.ts index 93073285c..c08636412 100644 --- a/__fixtures__/stage/extensions/@pgpm/faker/__tests__/faker.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/faker/__tests__/faker.test.ts @@ -31,6 +31,7 @@ it('gets random words', async () => { for (const type of types) { const { [type]: value } = await pg.one(`SELECT faker.${type}() AS ${type}`); + obj[type] = value; } console.log(obj); @@ -39,12 +40,14 @@ it('gets random words', async () => { it('lnglat', async () => { const obj: Record = {}; const { lnglat } = await pg.one(`SELECT faker.lnglat() AS lnglat`); + obj['lnglat'] = lnglat; const { lnglat: bbox } = await pg.one( `SELECT faker.lnglat($1, $2, $3, $4) AS lnglat`, [-118.561721, 33.59, -117.646374, 34.23302] ); + obj['bbox'] = bbox; console.log(obj); @@ -55,18 +58,21 @@ it('tags', async () => { const obj: Record = {}; const { tags } = await pg.one(`SELECT faker.tags() AS tags`); + obj['tags'] = tags; const { tag_with_min } = await pg.one( `SELECT faker.tags($1, $2, $3) AS tag_with_min`, [5, 10, 'tag'] ); + obj['tag with min'] = tag_with_min; const { face } = await pg.one( `SELECT faker.tags($1, $2, $3) AS face`, [5, 10, 'face'] ); + obj['face'] = face; console.log(obj); diff --git a/__fixtures__/stage/extensions/@pgpm/inflection/__tests__/inflection.test.ts b/__fixtures__/stage/extensions/@pgpm/inflection/__tests__/inflection.test.ts index 04efb53e4..b0b2577e0 100644 --- a/__fixtures__/stage/extensions/@pgpm/inflection/__tests__/inflection.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/inflection/__tests__/inflection.test.ts @@ -20,6 +20,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.pg_slugify($1, $2)', [opts.name, opts.allowUnicode] ); + expect(pg_slugify).toEqual(opts.result); }, [ @@ -43,6 +44,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.underscore($1)', [opts.name] ); + expect(underscore).toEqual(opts.result); }, [ @@ -64,6 +66,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.no_single_underscores($1)', [opts.name] ); + expect(no_single_underscores).toEqual(opts.result); }, [ @@ -78,6 +81,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.pascal($1)', [opts.name] ); + expect(pascal).toEqual(opts.result); }, [ @@ -102,6 +106,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.camel($1)', [opts.name] ); + expect(camel).toEqual(opts.result); }, [ @@ -127,6 +132,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.no_consecutive_caps($1)', [opts.name] ); + expect(no_consecutive_caps).toEqual(opts.result); }, [ @@ -144,6 +150,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.plural($1)', [opts.name] ); + expect(plural).toEqual(opts.result); }, [ @@ -165,6 +172,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.singular($1)', [opts.name] ); + expect(singular).toEqual(opts.result); }, [ diff --git a/__fixtures__/stage/extensions/@pgpm/jobs/__tests__/jobs.test.ts b/__fixtures__/stage/extensions/@pgpm/jobs/__tests__/jobs.test.ts index 45f08ffb2..641b790b6 100644 --- a/__fixtures__/stage/extensions/@pgpm/jobs/__tests__/jobs.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/jobs/__tests__/jobs.test.ts @@ -36,6 +36,7 @@ describe('scheduled jobs', () => { }, ] ); + objs.scheduled1 = result; }); @@ -53,6 +54,7 @@ describe('scheduled jobs', () => { { start, end, rule: '*/1 * * * *' } ] ); + objs.scheduled2 = result; }); @@ -63,6 +65,7 @@ describe('scheduled jobs', () => { ); const { queue_name, run_at, created_at, updated_at, ...obj } = result; + expect(obj).toMatchSnapshot(); }); diff --git a/__fixtures__/stage/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts b/__fixtures__/stage/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts index 854464d92..306d48fef 100644 --- a/__fixtures__/stage/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts @@ -55,6 +55,7 @@ it('get values', async () => { const { user_id } = await pg.one( `select jwt_public.current_user_id() as user_id` ); + await pg.any(`ROLLBACK`); expect({ user_agent }).toMatchSnapshot(); diff --git a/__fixtures__/stage/extensions/@pgpm/measurements/__tests__/measurements.test.ts b/__fixtures__/stage/extensions/@pgpm/measurements/__tests__/measurements.test.ts index cdfa4bff9..359887b99 100644 --- a/__fixtures__/stage/extensions/@pgpm/measurements/__tests__/measurements.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/measurements/__tests__/measurements.test.ts @@ -24,6 +24,7 @@ describe('measurements schema', () => { const schemas = await pg.any( `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'measurements'` ); + expect(schemas).toHaveLength(1); expect(schemas[0].schema_name).toBe('measurements'); }); diff --git a/__fixtures__/stage/extensions/@pgpm/totp/__tests__/algo.test.ts b/__fixtures__/stage/extensions/@pgpm/totp/__tests__/algo.test.ts index 44e3a97c9..d4f3d6773 100644 --- a/__fixtures__/stage/extensions/@pgpm/totp/__tests__/algo.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/totp/__tests__/algo.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -27,6 +27,7 @@ cases( )`, ['12345678901234567890', opts.len, opts.date, opts.algo] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, @@ -54,6 +55,7 @@ cases( )`, ['12345678901234567890', opts.len, opts.date, opts.algo, opts.step] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, @@ -78,6 +80,7 @@ cases( ) as verified`, ['12345678901234567890', opts.result, opts.step, opts.len, opts.date] ); + expect(verified).toBe(true); }, [ @@ -107,6 +110,7 @@ cases( )`, [opts.secret, opts.step, opts.len, opts.date, opts.algo, opts.encoding] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, diff --git a/__fixtures__/stage/extensions/@pgpm/totp/__tests__/totp.test.ts b/__fixtures__/stage/extensions/@pgpm/totp/__tests__/totp.test.ts index 460ce211c..7ac704879 100644 --- a/__fixtures__/stage/extensions/@pgpm/totp/__tests__/totp.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/totp/__tests__/totp.test.ts @@ -21,6 +21,7 @@ it('totp.generate + totp.verify basic', async () => { `SELECT totp.verify($1::text, $2::text) AS verify`, ['secret', generate] ); + expect(typeof generate).toBe('string'); expect(verify).toBe(true); }); diff --git a/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts b/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts index 7e01d98f1..81355336b 100644 --- a/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts @@ -122,6 +122,7 @@ describe('types', () => { it('invalid attachment and image', async () => { for (const attachment of invalidAttachments) { let failed = false; + try { await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); } catch (e) { @@ -132,6 +133,7 @@ describe('types', () => { for (const image of invalidImages) { let failed = false; + try { await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); } catch (e) { @@ -150,6 +152,7 @@ describe('types', () => { it('invalid upload', async () => { for (const upload of invalidUploads) { let failed = false; + try { await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); } catch (e) { @@ -168,6 +171,7 @@ describe('types', () => { it('invalid url', async () => { for (const value of invalidUrls) { let failed = false; + try { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); } catch (e) { @@ -190,6 +194,7 @@ describe('types', () => { it('not email', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (email) VALUES @@ -213,6 +218,7 @@ describe('types', () => { it('not hostname', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES @@ -225,6 +231,7 @@ describe('types', () => { it('not hostname 2', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES diff --git a/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.test.ts b/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.test.ts index 7e01d98f1..81355336b 100644 --- a/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/types/__tests__/domains.test.ts @@ -122,6 +122,7 @@ describe('types', () => { it('invalid attachment and image', async () => { for (const attachment of invalidAttachments) { let failed = false; + try { await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); } catch (e) { @@ -132,6 +133,7 @@ describe('types', () => { for (const image of invalidImages) { let failed = false; + try { await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); } catch (e) { @@ -150,6 +152,7 @@ describe('types', () => { it('invalid upload', async () => { for (const upload of invalidUploads) { let failed = false; + try { await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); } catch (e) { @@ -168,6 +171,7 @@ describe('types', () => { it('invalid url', async () => { for (const value of invalidUrls) { let failed = false; + try { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); } catch (e) { @@ -190,6 +194,7 @@ describe('types', () => { it('not email', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (email) VALUES @@ -213,6 +218,7 @@ describe('types', () => { it('not hostname', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES @@ -225,6 +231,7 @@ describe('types', () => { it('not hostname 2', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES diff --git a/__fixtures__/stage/extensions/@pgpm/types/__tests__/ext-types.test.ts b/__fixtures__/stage/extensions/@pgpm/types/__tests__/ext-types.test.ts index d156cb178..acf3aa963 100644 --- a/__fixtures__/stage/extensions/@pgpm/types/__tests__/ext-types.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/types/__tests__/ext-types.test.ts @@ -16,6 +16,7 @@ describe('@pgql/types', () => { const { typname } = await pg.one( `SELECT typname FROM pg_type WHERE typname = 'url'` ); + expect(typname).toBe('url'); }); }); diff --git a/__fixtures__/stage/extensions/@pgpm/utils/__tests__/utils.test.ts b/__fixtures__/stage/extensions/@pgpm/utils/__tests__/utils.test.ts index f5c69afb5..3c3b3313d 100644 --- a/__fixtures__/stage/extensions/@pgpm/utils/__tests__/utils.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/utils/__tests__/utils.test.ts @@ -16,6 +16,7 @@ it('more', async () => { `SELECT utils.mask_pad($1, $2) AS mask_pad`, ['101', 20] ); + expect(mask_pad).toMatchSnapshot(); }); @@ -24,6 +25,7 @@ it('less', async () => { `SELECT utils.mask_pad($1, $2) AS mask_pad`, ['101', 2] ); + expect(mask_pad).toMatchSnapshot(); }); @@ -33,6 +35,7 @@ describe('bitmask', () => { `SELECT utils.bitmask_pad($1::varbit, $2) AS bitmask_pad`, ['101', 20] ); + expect(bitmask_pad).toMatchSnapshot(); }); @@ -41,6 +44,7 @@ describe('bitmask', () => { `SELECT utils.bitmask_pad($1::varbit, $2) AS bitmask_pad`, ['101', 2] ); + expect(bitmask_pad).toMatchSnapshot(); }); }); diff --git a/__fixtures__/stage/extensions/@pgpm/uuid/__tests__/uuid.test.ts b/__fixtures__/stage/extensions/@pgpm/uuid/__tests__/uuid.test.ts index 801087811..143f8ac11 100644 --- a/__fixtures__/stage/extensions/@pgpm/uuid/__tests__/uuid.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/uuid/__tests__/uuid.test.ts @@ -47,6 +47,7 @@ describe('uuids.pseudo_order_uuid()', () => { const { pseudo_order_uuid } = await pg.one(` SELECT uuids.pseudo_order_uuid() AS pseudo_order_uuid `); + expect(pseudo_order_uuid).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -59,6 +60,7 @@ describe('uuids.pseudo_order_seed_uuid(seed)', () => { `SELECT uuids.pseudo_order_seed_uuid($1) AS pseudo_order_seed_uuid`, ['tenant123'] ); + expect(pseudo_order_seed_uuid).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -71,6 +73,7 @@ describe('uuids.trigger_set_uuid_seed', () => { `INSERT INTO public.items (name, seed) VALUES ($1, $2) RETURNING custom_id`, ['Item A', 'my-seed'] ); + expect(custom_id).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -83,6 +86,7 @@ describe('uuids.trigger_set_uuid_related_field', () => { `INSERT INTO public.items_seeded (tenant) VALUES ($1) RETURNING id`, ['tenant-42'] ); + expect(id).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); diff --git a/__fixtures__/stage/extensions/@pgpm/verify/__tests__/ext-verify.test.ts b/__fixtures__/stage/extensions/@pgpm/verify/__tests__/ext-verify.test.ts index eca3756d5..57fdfcf4e 100644 --- a/__fixtures__/stage/extensions/@pgpm/verify/__tests__/ext-verify.test.ts +++ b/__fixtures__/stage/extensions/@pgpm/verify/__tests__/ext-verify.test.ts @@ -1,4 +1,4 @@ -import { getConnections, PgTestClient, snapshot } from 'pgsql-test'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -34,6 +34,7 @@ describe('ext-verify utilities', () => { `SELECT get_entity_from_str($1) as entity`, [input] ); + expect(result.entity).toBe(expected); } }); @@ -51,6 +52,7 @@ describe('ext-verify utilities', () => { `SELECT get_schema_from_str($1) as schema_name`, [input] ); + expect(result.schema_name).toBe(expected); } }); @@ -61,6 +63,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_schema('public') as verified` ); + expect(result.verified).toBe(true); }); @@ -87,6 +90,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_table('test_table') as verified` ); + expect(result.verified).toBe(true); }); @@ -94,6 +98,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_table('public.test_table') as verified` ); + expect(result.verified).toBe(true); }); @@ -131,6 +136,7 @@ describe('ext-verify utilities', () => { `SELECT verify_constraint('test_constraints', $1) as verified`, [constraint.conname] ); + expect(result.verified).toBe(true); } }); @@ -158,6 +164,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_function('test_function') as verified` ); + expect(result.verified).toBe(true); }); @@ -165,6 +172,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_function('public.test_function') as verified` ); + expect(result.verified).toBe(true); }); @@ -194,6 +202,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_view('test_view') as verified` ); + expect(result.verified).toBe(true); }); @@ -201,6 +210,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_view('public.test_view') as verified` ); + expect(result.verified).toBe(true); }); @@ -230,6 +240,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_index('test_index_table', 'test_custom_index') as verified` ); + expect(result.verified).toBe(true); }); @@ -282,6 +293,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_trigger('test_update_trigger') as verified` ); + expect(result.verified).toBe(true); }); @@ -310,6 +322,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_type('test_enum') as verified` ); + expect(result.verified).toBe(true); }); @@ -317,6 +330,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_type('test_composite') as verified` ); + expect(result.verified).toBe(true); }); @@ -338,6 +352,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_domain('test_domain') as verified` ); + expect(result.verified).toBe(true); }); @@ -357,6 +372,7 @@ describe('ext-verify utilities', () => { `SELECT verify_role($1) as verified`, [currentUser.username] ); + expect(result.verified).toBe(true); }); @@ -377,6 +393,7 @@ describe('ext-verify utilities', () => { // Should at least include the user themselves expect(results.length).toBeGreaterThan(0); const usernames = results.map(r => r.rolname); + expect(usernames).toContain(currentUser.username); }); }); @@ -400,6 +417,7 @@ describe('ext-verify utilities', () => { `SELECT verify_table_grant('test_grants_table', 'INSERT', $1) as verified`, [currentUser.username] ); + expect(result.verified).toBe(true); }); }); @@ -418,6 +436,7 @@ describe('ext-verify utilities', () => { `SELECT verify_extension($1) as verified`, [availableExtensions[0].name] ); + expect(result.verified).toBe(true); } else { // Skip this test if no common extensions are available @@ -450,6 +469,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_security('test_security_table') as verified` ); + expect(result.verified).toBe(true); }); }); @@ -477,6 +497,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_policy('test_policy', 'test_policy_table') as verified` ); + expect(result.verified).toBe(true); }); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..bec10399f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,246 @@ +const eslint = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsparser = require('@typescript-eslint/parser'); +const simpleImportSort = require('eslint-plugin-simple-import-sort'); +const unusedImports = require('eslint-plugin-unused-imports'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + eslint.configs.recommended, + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/coverage/**', + '**/*.js', + '**/*.mjs', + '**/*.cjs', + '!eslint.config.js' + ] + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + global: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + setImmediate: 'readonly', + clearImmediate: 'readonly', + Promise: 'readonly', + Map: 'readonly', + Set: 'readonly', + WeakMap: 'readonly', + WeakSet: 'readonly', + Symbol: 'readonly', + Proxy: 'readonly', + Reflect: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + AbortController: 'readonly', + AbortSignal: 'readonly', + fetch: 'readonly', + Response: 'readonly', + Request: 'readonly', + Headers: 'readonly', + FormData: 'readonly', + Blob: 'readonly', + File: 'readonly', + ReadableStream: 'readonly', + WritableStream: 'readonly', + TransformStream: 'readonly', + queueMicrotask: 'readonly', + structuredClone: 'readonly', + crypto: 'readonly', + performance: 'readonly', + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + jest: 'readonly' + } + }, + plugins: { + '@typescript-eslint': tseslint, + 'simple-import-sort': simpleImportSort, + 'unused-imports': unusedImports + }, + rules: { + // TypeScript strict rules + ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'off', + '@typescript-eslint/no-unused-expressions': ['error', { + allowShortCircuit: true, + allowTernary: true + }], + + // Style rules - strict enforcement + 'indent': ['error', 2, { + SwitchCase: 1, + VariableDeclarator: 1, + outerIIFEBody: 1, + MemberExpression: 1, + FunctionDeclaration: { parameters: 1, body: 1 }, + FunctionExpression: { parameters: 1, body: 1 }, + CallExpression: { arguments: 1 }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoreComments: false + }], + 'quotes': ['error', 'single', { + avoidEscape: true, + allowTemplateLiterals: true + }], + 'semi': ['error', 'always'], + 'semi-spacing': ['error', { before: false, after: true }], + 'semi-style': ['error', 'last'], + 'comma-dangle': ['error', 'never'], + 'comma-spacing': ['error', { before: false, after: true }], + 'comma-style': ['error', 'last'], + 'quote-props': ['error', 'as-needed'], + + // Spacing rules + 'no-multi-spaces': 'error', + 'no-trailing-spaces': 'error', + 'no-whitespace-before-property': 'error', + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': ['error', { + anonymous: 'always', + named: 'never', + asyncArrow: 'always' + }], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': ['error', { words: true, nonwords: false }], + 'keyword-spacing': ['error', { before: true, after: true }], + 'key-spacing': ['error', { beforeColon: false, afterColon: true }], + 'arrow-spacing': ['error', { before: true, after: true }], + 'block-spacing': ['error', 'always'], + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], + 'computed-property-spacing': ['error', 'never'], + 'func-call-spacing': ['error', 'never'], + 'template-curly-spacing': ['error', 'never'], + 'rest-spread-spacing': ['error', 'never'], + 'switch-colon-spacing': ['error', { after: true, before: false }], + + // Brace and block rules + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'curly': ['error', 'multi-line', 'consistent'], + 'nonblock-statement-body-position': ['error', 'beside'], + + // Line rules + 'eol-last': ['error', 'always'], + 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0, maxBOF: 0 }], + 'linebreak-style': ['error', 'unix'], + 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] } + ], + + // Best practices + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'no-var': 'error', + 'prefer-const': ['error', { destructuring: 'all' }], + 'prefer-template': 'error', + 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'object-shorthand': ['error', 'always', { avoidQuotes: true }], + 'no-useless-concat': 'error', + 'no-useless-rename': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': 'error', + 'no-unneeded-ternary': ['error', { defaultAssignment: false }], + 'no-lonely-if': 'error', + 'no-else-return': ['error', { allowElseIf: false }], + 'no-throw-literal': 'error', + 'no-return-await': 'error', + 'no-await-in-loop': 'warn', + 'require-await': 'off', + '@typescript-eslint/require-await': 'off', + 'no-async-promise-executor': 'error', + 'no-promise-executor-return': 'error', + 'prefer-promise-reject-errors': 'error', + + // Error prevention + 'no-duplicate-imports': 'error', + 'no-self-compare': 'error', + 'no-template-curly-in-string': 'warn', + 'no-unreachable-loop': 'error', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': ['error', { + functions: false, + classes: true, + variables: true + }], + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error', { + ignoreTypeValueShadow: true, + ignoreFunctionTypeParameterNameValueShadow: true + }], + + // Switch statements + 'default-case': 'warn', + 'default-case-last': 'error', + 'no-fallthrough': 'error', + + // Console and debugging + 'no-console': 'off', + 'no-debugger': 'error', + 'no-alert': 'error', + + // Import sorting + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + 'unused-imports/no-unused-imports': 'error', + + // Disabled rules that conflict with TypeScript or are too strict + 'no-undef': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'error', + 'no-dupe-class-members': 'off', + '@typescript-eslint/no-dupe-class-members': 'error', + 'no-case-declarations': 'off', + 'no-implicit-globals': 'off' + } + }, + prettierConfig +]; diff --git a/extensions/@pgpm/achievements/__tests__/achievements.test.ts b/extensions/@pgpm/achievements/__tests__/achievements.test.ts index 6bc1b904f..afa1048cc 100644 --- a/extensions/@pgpm/achievements/__tests__/achievements.test.ts +++ b/extensions/@pgpm/achievements/__tests__/achievements.test.ts @@ -65,6 +65,7 @@ it('newbie', async () => { 'accept_privacy', ...repeat('complete_action', 3) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -76,24 +77,28 @@ it('newbie', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -106,6 +111,7 @@ it('advanced', async () => { ...repeat('invite_users', 3), ...repeat('complete_action', 21) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -117,24 +123,28 @@ it('advanced', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -157,6 +167,7 @@ it('advanced part II', async () => { ...repeat('invite_users', 3), ...repeat('complete_action', 10) ]; + for (const name of steps) { await pg.any( `INSERT INTO status_public.user_steps (user_id, name) VALUES ($1, $2)`, @@ -168,24 +179,28 @@ it('advanced part II', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); @@ -227,23 +242,27 @@ it('advanced part III', async () => { `SELECT * FROM status_public.steps_required($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advancedRequirements })).toMatchSnapshot(); const newbieRequirements = await pg.any( `SELECT * FROM status_public.steps_required($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbieRequirements })).toMatchSnapshot(); const [userAchievedNewbie] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['newbie', user_id] ); + expect(snapshot({ newbie: userAchievedNewbie })).toMatchSnapshot(); const [userAchievedAdvanced] = await pg.any( `SELECT * FROM status_public.user_achieved($1, $2)`, ['advanced', user_id] ); + expect(snapshot({ advanced: userAchievedAdvanced })).toMatchSnapshot(); }); diff --git a/extensions/@pgpm/achievements/__tests__/triggers.test.ts b/extensions/@pgpm/achievements/__tests__/triggers.test.ts index 877408bad..81ba8f556 100644 --- a/extensions/@pgpm/achievements/__tests__/triggers.test.ts +++ b/extensions/@pgpm/achievements/__tests__/triggers.test.ts @@ -117,6 +117,7 @@ it('newbie', async () => { const beforeInsert = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ beforeInsert })).toMatchSnapshot(); await pg.any( @@ -127,6 +128,7 @@ it('newbie', async () => { const afterFirstInsert = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterFirstInsert })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET toggle = 'yo'`); @@ -134,6 +136,7 @@ it('newbie', async () => { const afterUpdateToggleToValue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterUpdateToggleToValue })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET toggle = NULL`); @@ -141,6 +144,7 @@ it('newbie', async () => { const afterUpdateToggleToNull = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterUpdateToggleToNull })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_verified = TRUE`); @@ -148,6 +152,7 @@ it('newbie', async () => { const afterIsVerifiedIsTrue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsVerifiedIsTrue })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_verified = FALSE`); @@ -155,6 +160,7 @@ it('newbie', async () => { const afterIsVerifiedIsFalse = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsVerifiedIsFalse })).toMatchSnapshot(); await pg.any(`UPDATE status_public.mytable SET is_approved = TRUE`); @@ -162,5 +168,6 @@ it('newbie', async () => { const afterIsApprovedTrue = await pg.any( `SELECT * FROM status_public.user_achievements ORDER BY name` ); + expect(snapshot({ afterIsApprovedTrue })).toMatchSnapshot(); }); diff --git a/extensions/@pgpm/base32/__tests__/base32.decode.test.ts b/extensions/@pgpm/base32/__tests__/base32.decode.test.ts index 13d30b244..b03c22a43 100644 --- a/extensions/@pgpm/base32/__tests__/base32.decode.test.ts +++ b/extensions/@pgpm/base32/__tests__/base32.decode.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -18,6 +18,7 @@ it('base32_to_decimal', async () => { `SELECT base32.base32_to_decimal($1::text) AS base32_to_decimal`, ['INQXI==='] ); + expect(base32_to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']); }); @@ -26,6 +27,7 @@ it('decimal_to_chunks', async () => { `SELECT base32.decimal_to_chunks($1::text[]) AS decimal_to_chunks`, [['8', '13', '16', '23', '8', '=', '=', '=']] ); + expect(decimal_to_chunks).toEqual([ '01000', '01101', @@ -43,6 +45,7 @@ it('decode', async () => { `SELECT base32.decode($1::text) AS decode`, ['INQXI'] ); + expect(decode).toEqual('Cat'); }); @@ -51,6 +54,7 @@ it('zero_fill', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [300, 2] ); + expect(zero_fill).toBe('75'); }); @@ -59,6 +63,7 @@ it('zero_fill (-)', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [-300, 2] ); + expect(zero_fill).toBe('1073741749'); }); @@ -67,6 +72,7 @@ it('zero_fill (0)', async () => { `SELECT base32.zero_fill($1::int, $2::int) AS zero_fill`, [-300, 0] ); + expect(zero_fill).toBe('4294966996'); }); @@ -77,6 +83,7 @@ cases( `SELECT base32.decode($1::text) AS decode`, [opts.name] ); + expect(decode).toEqual(opts.result); expect(decode).toMatchSnapshot(); }, diff --git a/extensions/@pgpm/base32/__tests__/base32.encode.test.ts b/extensions/@pgpm/base32/__tests__/base32.encode.test.ts index c807f5356..aaaabee41 100644 --- a/extensions/@pgpm/base32/__tests__/base32.encode.test.ts +++ b/extensions/@pgpm/base32/__tests__/base32.encode.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -18,6 +18,7 @@ it('to_ascii', async () => { `SELECT base32.to_ascii($1::text) AS to_ascii`, ['Cat'] ); + expect(to_ascii).toEqual([67, 97, 116]); }); @@ -30,6 +31,7 @@ it('to_binary', async () => { `SELECT base32.to_binary($1::int[]) AS to_binary`, [to_ascii] ); + expect(to_binary).toEqual(['01000011', '01100001', '01110100']); }); @@ -38,6 +40,7 @@ it('to_groups', async () => { `SELECT base32.to_groups($1::text[]) AS to_groups`, [['01000011', '01100001', '01110100']] ); + expect(to_groups).toEqual([ '01000011', '01100001', @@ -52,6 +55,7 @@ it('to_chunks', async () => { `SELECT base32.to_chunks($1::text[]) AS to_chunks`, [['01000011', '01100001', '01110100', 'xxxxxxxx', 'xxxxxxxx']] ); + expect(to_chunks).toEqual([ '01000', '01101', @@ -78,6 +82,7 @@ it('fill_chunks', async () => { 'xxxxx' ]] ); + expect(fill_chunks).toEqual([ '01000', '01101', @@ -104,6 +109,7 @@ it('to_decimal', async () => { 'xxxxx' ]] ); + expect(to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']); }); @@ -112,6 +118,7 @@ it('to_base32', async () => { `SELECT base32.to_base32($1::text[]) AS to_base32`, [['8', '13', '16', '23', '8', '=', '=', '=']] ); + expect(to_base32).toEqual('INQXI==='); }); @@ -122,6 +129,7 @@ cases( `SELECT base32.encode($1::text) AS encode`, [opts.name] ); + expect(encode).toEqual(opts.result); expect(encode).toMatchSnapshot(); }, diff --git a/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts b/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts index 453de4c04..f38f026cb 100644 --- a/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts +++ b/extensions/@pgpm/database-jobs/__tests__/jobs.test.ts @@ -30,6 +30,7 @@ describe('scheduled jobs', () => { } ] ); + objs.scheduled1 = result; }); @@ -48,6 +49,7 @@ describe('scheduled jobs', () => { { start, end, rule: '*/1 * * * *' } ] ); + objs.scheduled2 = result; }); @@ -58,6 +60,7 @@ describe('scheduled jobs', () => { ); const { queue_name, run_at, created_at, updated_at, ...obj } = result; + expect(obj).toMatchSnapshot(); }); diff --git a/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts b/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts index fc95af6cb..941cb8915 100644 --- a/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts +++ b/extensions/@pgpm/db-meta-modules/__tests__/modules.test.ts @@ -78,6 +78,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -103,6 +104,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -128,6 +130,7 @@ describe('db_meta_modules', () => { // Check that key columns exist const columnNames = columns.map(c => c.column_name); + expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); expect(columnNames).toContain('schema_id'); @@ -215,6 +218,7 @@ describe('db_meta_modules', () => { // Group by foreign table to see what they reference const foreignTables = [...new Set(fkConstraints.map(fk => fk.foreign_table_name))]; + expect(foreignTables).toContain('database'); expect(foreignTables).toContain('schema'); expect(foreignTables).toContain('table'); diff --git a/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts b/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts index 2ed050a3a..021b71690 100644 --- a/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts +++ b/extensions/@pgpm/db-meta-schema/__tests__/meta.test.ts @@ -43,6 +43,7 @@ describe('db_meta functionality', () => { ...obj, dbname: 'test-database' // Replace dynamic dbname with static value }; + expect(snapshot(normalized)).toMatchSnapshot(); }; @@ -53,8 +54,10 @@ describe('db_meta functionality', () => { RETURNING *`, [owner_id, 'my-meta-db'] ); + objs.db = database; const database_id = database.id; + expect(snapshot(database)).toMatchSnapshot(); // Step 2: Create APIs first (since domains reference them) @@ -64,6 +67,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'public', 'authenticated', 'anonymous'] ); + objs.apis.public = publicApi; snapWithNormalizedDbname(publicApi); @@ -73,6 +77,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'admin', 'administrator', 'administrator'] ); + objs.apis.admin = adminApi; snapWithNormalizedDbname(adminApi); @@ -83,6 +88,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'Website Title', 'Website Description'] ); + objs.sites.app = appSite; snapWithNormalizedDbname(appSite); @@ -93,6 +99,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.apis.public.id, 'pgpm.io', 'api'] ); + objs.domains.api = apiDomain; expect(snapshot(apiDomain)).toMatchSnapshot(); @@ -102,6 +109,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.sites.app.id, 'pgpm.io', 'app'] ); + objs.domains.app = appDomain; expect(snapshot(appDomain)).toMatchSnapshot(); @@ -111,6 +119,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, objs.apis.admin.id, 'pgpm.io', 'admin'] ); + objs.domains.admin = adminDomain; expect(snapshot(adminDomain)).toMatchSnapshot(); @@ -120,6 +129,7 @@ describe('db_meta functionality', () => { RETURNING *`, [database_id, 'pgpm.io'] ); + objs.domains.base = baseDomain; // Step 5: Register modules @@ -131,6 +141,7 @@ describe('db_meta functionality', () => { supportEmail: 'support@interweb.co' })] ); + expect(snapshot(siteModule1)).toMatchSnapshot(); const [apiModule] = await pg.any( @@ -142,6 +153,7 @@ describe('db_meta functionality', () => { authenticate: 'authenticate' })] ); + expect(snapshot(apiModule)).toMatchSnapshot(); const [siteModule2] = await pg.any( @@ -159,6 +171,7 @@ describe('db_meta functionality', () => { verify_email: 'verify_email' })] ); + expect(snapshot(siteModule2)).toMatchSnapshot(); // Step 6: Schema associations diff --git a/extensions/@pgpm/defaults/__tests__/defaults.test.ts b/extensions/@pgpm/defaults/__tests__/defaults.test.ts index 9bcdcf54f..45ced3549 100644 --- a/extensions/@pgpm/defaults/__tests__/defaults.test.ts +++ b/extensions/@pgpm/defaults/__tests__/defaults.test.ts @@ -39,6 +39,7 @@ describe('defaults security configurations', () => { // If datacl exists, check that PUBLIC doesn't appear or has no privileges if (privileges[0].datacl) { const aclString = privileges[0].datacl.toString(); + // PUBLIC should not appear in the ACL with any privileges // The ACL format is {user=privileges/grantor,...} // PUBLIC would appear as "=privileges/grantor" or "PUBLIC=privileges/grantor" @@ -50,10 +51,12 @@ describe('defaults security configurations', () => { it('should verify database connection still works for current user', async () => { // Verify we can still connect and run queries const [result] = await pg.any(`SELECT 1 as test`); + expect(result.test).toBe(1); // Verify we can see current user const [userResult] = await pg.any(`SELECT current_user as username`); + expect(userResult.username).toBeDefined(); }); }); @@ -84,6 +87,7 @@ describe('defaults security configurations', () => { it('should allow current user to execute functions they create', async () => { // Current user should still be able to execute their own functions const [result] = await pg.any(`SELECT test_default_function() as result`); + expect(result.result).toBe('test result'); }); @@ -109,6 +113,7 @@ describe('defaults security configurations', () => { for (const priv of defaultPrivs) { if (priv.defaclacl) { const aclString = priv.defaclacl.toString(); + // Check that PUBLIC doesn't have execute (X) privileges // PUBLIC would appear as "=X/grantor" or "PUBLIC=X/grantor" expect(aclString).not.toMatch(/(?:^|,)(?:PUBLIC)?=[^,}]*X[^,}]*(?:,|$)/); @@ -177,6 +182,7 @@ describe('defaults security configurations', () => { const [result] = await pg.any(` SELECT secret_data FROM test_security_table LIMIT 1 `); + expect(result.secret_data).toBe('confidential information'); // Check table privileges for PUBLIC @@ -287,6 +293,7 @@ describe('defaults security configurations', () => { FROM pg_default_acl WHERE defaclobjtype = 'f' `); + // Should have some default ACL configuration expect(parseInt(defaultFuncPrivs[0].count)).toBeGreaterThanOrEqual(0); @@ -295,6 +302,7 @@ describe('defaults security configurations', () => { SELECT has_schema_privilege('public', 'public', 'create') as public_can_create_in_public `); + expect(schemaPrivs.public_can_create_in_public).toBe(false); }); diff --git a/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts b/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts index 197c08aab..fd989ad58 100644 --- a/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts +++ b/extensions/@pgpm/encrypted-secrets-table/__tests__/secrets-table.test.ts @@ -27,6 +27,7 @@ describe('encrypted secrets table', () => { const schemas = await pg.any( `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'secrets_schema'` ); + expect(schemas).toHaveLength(1); expect(schemas[0].schema_name).toBe('secrets_schema'); }); @@ -128,6 +129,7 @@ describe('encrypted secrets table', () => { // Verify it's a bcrypt hash (starts with $2) const hashedValue = result.secrets_value_field.toString(); + expect(hashedValue).toMatch(/^\$2[aby]?\$/); }); @@ -191,6 +193,7 @@ describe('encrypted secrets table', () => { // Verify it's a bcrypt hash const hashedValue = updated.secrets_value_field.toString(); + expect(hashedValue).toMatch(/^\$2[aby]?\$/); }); @@ -234,6 +237,7 @@ describe('encrypted secrets table', () => { // The stored hash should be different from the original password const storedHash = result.secrets_value_field.toString(); + expect(storedHash).not.toBe(password); // Verify it's a proper bcrypt hash format @@ -252,6 +256,7 @@ describe('encrypted secrets table', () => { ); const storedHash2 = result2.secrets_value_field.toString(); + expect(storedHash2).not.toBe(storedHash); // Different salt = different hash expect(storedHash2).toMatch(/^\$2[aby]?\$/); }); diff --git a/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts b/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts index 3d3acaf27..9f7724588 100644 --- a/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts +++ b/extensions/@pgpm/encrypted-secrets/__tests__/secrets.test.ts @@ -45,6 +45,7 @@ describe('encrypted secrets', () => { WHERE secrets_owned_field = $1`, [user_id] ); + expect(encrypt_field_pgp_get).toMatchSnapshot(); }); @@ -52,6 +53,7 @@ describe('encrypted secrets', () => { const [{ encrypt_field_set }] = await pg.any( `SELECT encrypted_secrets.encrypt_field_set('myvalue')` ); + expect(encrypt_field_set).toMatchSnapshot(); }); @@ -61,6 +63,7 @@ describe('encrypted secrets', () => { encrypted_secrets.encrypt_field_set('value-there-and-back') )` ); + expect(encrypt_field_bytea_to_text).toMatchSnapshot(); }); @@ -72,6 +75,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_getter).toMatchSnapshot(); }); @@ -84,6 +88,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_verify).toMatchSnapshot(); }); @@ -96,6 +101,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_upsert).toMatchSnapshot(); const [{ secrets_verify }] = await pg.any( @@ -106,6 +112,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(secrets_verify).toMatchSnapshot(); }); @@ -115,6 +122,7 @@ describe('encrypted secrets', () => { `SELECT encrypted_secrets.secrets_getter($1::uuid, 'my-secret-name')`, [user_id] ); + expect(beforeDelete.secrets_getter).toBe('my-secret'); // Delete the secret @@ -128,6 +136,7 @@ describe('encrypted secrets', () => { `SELECT encrypted_secrets.secrets_getter($1::uuid, 'my-secret-name', 'default-value')`, [user_id] ); + expect(afterDelete.secrets_getter).toBe('default-value'); }); @@ -171,6 +180,7 @@ describe('encrypted secrets', () => { )`, [user_id] ); + expect(encrypt_field_pgp_getter).toMatchSnapshot(); }); @@ -188,6 +198,7 @@ describe('encrypted secrets', () => { }) ] ); + expect(secrets_table_upsert).toMatchSnapshot(); }); }); diff --git a/extensions/@pgpm/faker/__tests__/faker.test.ts b/extensions/@pgpm/faker/__tests__/faker.test.ts index 93073285c..c08636412 100644 --- a/extensions/@pgpm/faker/__tests__/faker.test.ts +++ b/extensions/@pgpm/faker/__tests__/faker.test.ts @@ -31,6 +31,7 @@ it('gets random words', async () => { for (const type of types) { const { [type]: value } = await pg.one(`SELECT faker.${type}() AS ${type}`); + obj[type] = value; } console.log(obj); @@ -39,12 +40,14 @@ it('gets random words', async () => { it('lnglat', async () => { const obj: Record = {}; const { lnglat } = await pg.one(`SELECT faker.lnglat() AS lnglat`); + obj['lnglat'] = lnglat; const { lnglat: bbox } = await pg.one( `SELECT faker.lnglat($1, $2, $3, $4) AS lnglat`, [-118.561721, 33.59, -117.646374, 34.23302] ); + obj['bbox'] = bbox; console.log(obj); @@ -55,18 +58,21 @@ it('tags', async () => { const obj: Record = {}; const { tags } = await pg.one(`SELECT faker.tags() AS tags`); + obj['tags'] = tags; const { tag_with_min } = await pg.one( `SELECT faker.tags($1, $2, $3) AS tag_with_min`, [5, 10, 'tag'] ); + obj['tag with min'] = tag_with_min; const { face } = await pg.one( `SELECT faker.tags($1, $2, $3) AS face`, [5, 10, 'face'] ); + obj['face'] = face; console.log(obj); diff --git a/extensions/@pgpm/inflection/__tests__/inflection.test.ts b/extensions/@pgpm/inflection/__tests__/inflection.test.ts index 04efb53e4..b0b2577e0 100644 --- a/extensions/@pgpm/inflection/__tests__/inflection.test.ts +++ b/extensions/@pgpm/inflection/__tests__/inflection.test.ts @@ -20,6 +20,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.pg_slugify($1, $2)', [opts.name, opts.allowUnicode] ); + expect(pg_slugify).toEqual(opts.result); }, [ @@ -43,6 +44,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.underscore($1)', [opts.name] ); + expect(underscore).toEqual(opts.result); }, [ @@ -64,6 +66,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.no_single_underscores($1)', [opts.name] ); + expect(no_single_underscores).toEqual(opts.result); }, [ @@ -78,6 +81,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.pascal($1)', [opts.name] ); + expect(pascal).toEqual(opts.result); }, [ @@ -102,6 +106,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.camel($1)', [opts.name] ); + expect(camel).toEqual(opts.result); }, [ @@ -127,6 +132,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.no_consecutive_caps($1)', [opts.name] ); + expect(no_consecutive_caps).toEqual(opts.result); }, [ @@ -144,6 +150,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.plural($1)', [opts.name] ); + expect(plural).toEqual(opts.result); }, [ @@ -165,6 +172,7 @@ describe('inflection', () => { 'SELECT * FROM inflection.singular($1)', [opts.name] ); + expect(singular).toEqual(opts.result); }, [ diff --git a/extensions/@pgpm/jobs/__tests__/jobs.test.ts b/extensions/@pgpm/jobs/__tests__/jobs.test.ts index 45f08ffb2..641b790b6 100644 --- a/extensions/@pgpm/jobs/__tests__/jobs.test.ts +++ b/extensions/@pgpm/jobs/__tests__/jobs.test.ts @@ -36,6 +36,7 @@ describe('scheduled jobs', () => { }, ] ); + objs.scheduled1 = result; }); @@ -53,6 +54,7 @@ describe('scheduled jobs', () => { { start, end, rule: '*/1 * * * *' } ] ); + objs.scheduled2 = result; }); @@ -63,6 +65,7 @@ describe('scheduled jobs', () => { ); const { queue_name, run_at, created_at, updated_at, ...obj } = result; + expect(obj).toMatchSnapshot(); }); diff --git a/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts b/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts index 854464d92..306d48fef 100644 --- a/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts +++ b/extensions/@pgpm/jwt-claims/__tests__/jwt.test.ts @@ -55,6 +55,7 @@ it('get values', async () => { const { user_id } = await pg.one( `select jwt_public.current_user_id() as user_id` ); + await pg.any(`ROLLBACK`); expect({ user_agent }).toMatchSnapshot(); diff --git a/extensions/@pgpm/measurements/__tests__/measurements.test.ts b/extensions/@pgpm/measurements/__tests__/measurements.test.ts index cdfa4bff9..359887b99 100644 --- a/extensions/@pgpm/measurements/__tests__/measurements.test.ts +++ b/extensions/@pgpm/measurements/__tests__/measurements.test.ts @@ -24,6 +24,7 @@ describe('measurements schema', () => { const schemas = await pg.any( `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'measurements'` ); + expect(schemas).toHaveLength(1); expect(schemas[0].schema_name).toBe('measurements'); }); diff --git a/extensions/@pgpm/totp/__tests__/algo.test.ts b/extensions/@pgpm/totp/__tests__/algo.test.ts index 44e3a97c9..d4f3d6773 100644 --- a/extensions/@pgpm/totp/__tests__/algo.test.ts +++ b/extensions/@pgpm/totp/__tests__/algo.test.ts @@ -1,5 +1,5 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; import cases from 'jest-in-case'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -27,6 +27,7 @@ cases( )`, ['12345678901234567890', opts.len, opts.date, opts.algo] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, @@ -54,6 +55,7 @@ cases( )`, ['12345678901234567890', opts.len, opts.date, opts.algo, opts.step] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, @@ -78,6 +80,7 @@ cases( ) as verified`, ['12345678901234567890', opts.result, opts.step, opts.len, opts.date] ); + expect(verified).toBe(true); }, [ @@ -107,6 +110,7 @@ cases( )`, [opts.secret, opts.step, opts.len, opts.date, opts.algo, opts.encoding] ); + expect(generate).toEqual(opts.result); expect(generate).toMatchSnapshot(); }, diff --git a/extensions/@pgpm/totp/__tests__/totp.test.ts b/extensions/@pgpm/totp/__tests__/totp.test.ts index 460ce211c..7ac704879 100644 --- a/extensions/@pgpm/totp/__tests__/totp.test.ts +++ b/extensions/@pgpm/totp/__tests__/totp.test.ts @@ -21,6 +21,7 @@ it('totp.generate + totp.verify basic', async () => { `SELECT totp.verify($1::text, $2::text) AS verify`, ['secret', generate] ); + expect(typeof generate).toBe('string'); expect(verify).toBe(true); }); diff --git a/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts b/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts index 7e01d98f1..81355336b 100644 --- a/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts +++ b/extensions/@pgpm/types/__tests__/domains.pgutils.test.ts @@ -122,6 +122,7 @@ describe('types', () => { it('invalid attachment and image', async () => { for (const attachment of invalidAttachments) { let failed = false; + try { await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); } catch (e) { @@ -132,6 +133,7 @@ describe('types', () => { for (const image of invalidImages) { let failed = false; + try { await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); } catch (e) { @@ -150,6 +152,7 @@ describe('types', () => { it('invalid upload', async () => { for (const upload of invalidUploads) { let failed = false; + try { await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); } catch (e) { @@ -168,6 +171,7 @@ describe('types', () => { it('invalid url', async () => { for (const value of invalidUrls) { let failed = false; + try { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); } catch (e) { @@ -190,6 +194,7 @@ describe('types', () => { it('not email', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (email) VALUES @@ -213,6 +218,7 @@ describe('types', () => { it('not hostname', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES @@ -225,6 +231,7 @@ describe('types', () => { it('not hostname 2', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES diff --git a/extensions/@pgpm/types/__tests__/domains.test.ts b/extensions/@pgpm/types/__tests__/domains.test.ts index 7e01d98f1..81355336b 100644 --- a/extensions/@pgpm/types/__tests__/domains.test.ts +++ b/extensions/@pgpm/types/__tests__/domains.test.ts @@ -122,6 +122,7 @@ describe('types', () => { it('invalid attachment and image', async () => { for (const attachment of invalidAttachments) { let failed = false; + try { await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); } catch (e) { @@ -132,6 +133,7 @@ describe('types', () => { for (const image of invalidImages) { let failed = false; + try { await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); } catch (e) { @@ -150,6 +152,7 @@ describe('types', () => { it('invalid upload', async () => { for (const upload of invalidUploads) { let failed = false; + try { await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); } catch (e) { @@ -168,6 +171,7 @@ describe('types', () => { it('invalid url', async () => { for (const value of invalidUrls) { let failed = false; + try { await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); } catch (e) { @@ -190,6 +194,7 @@ describe('types', () => { it('not email', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (email) VALUES @@ -213,6 +218,7 @@ describe('types', () => { it('not hostname', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES @@ -225,6 +231,7 @@ describe('types', () => { it('not hostname 2', async () => { let failed = false; + try { await pg.any(` INSERT INTO customers (domain) VALUES diff --git a/extensions/@pgpm/types/__tests__/ext-types.test.ts b/extensions/@pgpm/types/__tests__/ext-types.test.ts index d156cb178..acf3aa963 100644 --- a/extensions/@pgpm/types/__tests__/ext-types.test.ts +++ b/extensions/@pgpm/types/__tests__/ext-types.test.ts @@ -16,6 +16,7 @@ describe('@pgql/types', () => { const { typname } = await pg.one( `SELECT typname FROM pg_type WHERE typname = 'url'` ); + expect(typname).toBe('url'); }); }); diff --git a/extensions/@pgpm/utils/__tests__/utils.test.ts b/extensions/@pgpm/utils/__tests__/utils.test.ts index f5c69afb5..3c3b3313d 100644 --- a/extensions/@pgpm/utils/__tests__/utils.test.ts +++ b/extensions/@pgpm/utils/__tests__/utils.test.ts @@ -16,6 +16,7 @@ it('more', async () => { `SELECT utils.mask_pad($1, $2) AS mask_pad`, ['101', 20] ); + expect(mask_pad).toMatchSnapshot(); }); @@ -24,6 +25,7 @@ it('less', async () => { `SELECT utils.mask_pad($1, $2) AS mask_pad`, ['101', 2] ); + expect(mask_pad).toMatchSnapshot(); }); @@ -33,6 +35,7 @@ describe('bitmask', () => { `SELECT utils.bitmask_pad($1::varbit, $2) AS bitmask_pad`, ['101', 20] ); + expect(bitmask_pad).toMatchSnapshot(); }); @@ -41,6 +44,7 @@ describe('bitmask', () => { `SELECT utils.bitmask_pad($1::varbit, $2) AS bitmask_pad`, ['101', 2] ); + expect(bitmask_pad).toMatchSnapshot(); }); }); diff --git a/extensions/@pgpm/uuid/__tests__/uuid.test.ts b/extensions/@pgpm/uuid/__tests__/uuid.test.ts index 801087811..143f8ac11 100644 --- a/extensions/@pgpm/uuid/__tests__/uuid.test.ts +++ b/extensions/@pgpm/uuid/__tests__/uuid.test.ts @@ -47,6 +47,7 @@ describe('uuids.pseudo_order_uuid()', () => { const { pseudo_order_uuid } = await pg.one(` SELECT uuids.pseudo_order_uuid() AS pseudo_order_uuid `); + expect(pseudo_order_uuid).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -59,6 +60,7 @@ describe('uuids.pseudo_order_seed_uuid(seed)', () => { `SELECT uuids.pseudo_order_seed_uuid($1) AS pseudo_order_seed_uuid`, ['tenant123'] ); + expect(pseudo_order_seed_uuid).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -71,6 +73,7 @@ describe('uuids.trigger_set_uuid_seed', () => { `INSERT INTO public.items (name, seed) VALUES ($1, $2) RETURNING custom_id`, ['Item A', 'my-seed'] ); + expect(custom_id).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); @@ -83,6 +86,7 @@ describe('uuids.trigger_set_uuid_related_field', () => { `INSERT INTO public.items_seeded (tenant) VALUES ($1) RETURNING id`, ['tenant-42'] ); + expect(id).toMatch( /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/ ); diff --git a/extensions/@pgpm/verify/__tests__/ext-verify.test.ts b/extensions/@pgpm/verify/__tests__/ext-verify.test.ts index eca3756d5..57fdfcf4e 100644 --- a/extensions/@pgpm/verify/__tests__/ext-verify.test.ts +++ b/extensions/@pgpm/verify/__tests__/ext-verify.test.ts @@ -1,4 +1,4 @@ -import { getConnections, PgTestClient, snapshot } from 'pgsql-test'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; @@ -34,6 +34,7 @@ describe('ext-verify utilities', () => { `SELECT get_entity_from_str($1) as entity`, [input] ); + expect(result.entity).toBe(expected); } }); @@ -51,6 +52,7 @@ describe('ext-verify utilities', () => { `SELECT get_schema_from_str($1) as schema_name`, [input] ); + expect(result.schema_name).toBe(expected); } }); @@ -61,6 +63,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_schema('public') as verified` ); + expect(result.verified).toBe(true); }); @@ -87,6 +90,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_table('test_table') as verified` ); + expect(result.verified).toBe(true); }); @@ -94,6 +98,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_table('public.test_table') as verified` ); + expect(result.verified).toBe(true); }); @@ -131,6 +136,7 @@ describe('ext-verify utilities', () => { `SELECT verify_constraint('test_constraints', $1) as verified`, [constraint.conname] ); + expect(result.verified).toBe(true); } }); @@ -158,6 +164,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_function('test_function') as verified` ); + expect(result.verified).toBe(true); }); @@ -165,6 +172,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_function('public.test_function') as verified` ); + expect(result.verified).toBe(true); }); @@ -194,6 +202,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_view('test_view') as verified` ); + expect(result.verified).toBe(true); }); @@ -201,6 +210,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_view('public.test_view') as verified` ); + expect(result.verified).toBe(true); }); @@ -230,6 +240,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_index('test_index_table', 'test_custom_index') as verified` ); + expect(result.verified).toBe(true); }); @@ -282,6 +293,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_trigger('test_update_trigger') as verified` ); + expect(result.verified).toBe(true); }); @@ -310,6 +322,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_type('test_enum') as verified` ); + expect(result.verified).toBe(true); }); @@ -317,6 +330,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_type('test_composite') as verified` ); + expect(result.verified).toBe(true); }); @@ -338,6 +352,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_domain('test_domain') as verified` ); + expect(result.verified).toBe(true); }); @@ -357,6 +372,7 @@ describe('ext-verify utilities', () => { `SELECT verify_role($1) as verified`, [currentUser.username] ); + expect(result.verified).toBe(true); }); @@ -377,6 +393,7 @@ describe('ext-verify utilities', () => { // Should at least include the user themselves expect(results.length).toBeGreaterThan(0); const usernames = results.map(r => r.rolname); + expect(usernames).toContain(currentUser.username); }); }); @@ -400,6 +417,7 @@ describe('ext-verify utilities', () => { `SELECT verify_table_grant('test_grants_table', 'INSERT', $1) as verified`, [currentUser.username] ); + expect(result.verified).toBe(true); }); }); @@ -418,6 +436,7 @@ describe('ext-verify utilities', () => { `SELECT verify_extension($1) as verified`, [availableExtensions[0].name] ); + expect(result.verified).toBe(true); } else { // Skip this test if no common extensions are available @@ -450,6 +469,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_security('test_security_table') as verified` ); + expect(result.verified).toBe(true); }); }); @@ -477,6 +497,7 @@ describe('ext-verify utilities', () => { const [result] = await pg.any( `SELECT verify_policy('test_policy', 'test_policy_table') as verified` ); + expect(result.verified).toBe(true); }); diff --git a/functions/send-email-link/src/index.ts b/functions/send-email-link/src/index.ts index 47c35c1ec..e236d614b 100644 --- a/functions/send-email-link/src/index.ts +++ b/functions/send-email-link/src/index.ts @@ -1,9 +1,9 @@ import app from '@constructive-io/knative-job-fn'; -import { GraphQLClient } from 'graphql-request'; -import gql from 'graphql-tag'; import { generate } from '@launchql/mjml'; import { send } from '@launchql/postmaster'; import { parseEnvBoolean } from '@pgpmjs/env'; +import { GraphQLClient } from 'graphql-request'; +import gql from 'graphql-tag'; const isDryRun = parseEnvBoolean(process.env.SEND_EMAIL_LINK_DRY_RUN) ?? false; @@ -65,9 +65,11 @@ type GraphQLContext = { const getRequiredEnv = (name: string): string => { const value = process.env[name]; + if (!value) { throw new Error(`Missing required environment variable ${name}`); } + return value; }; @@ -83,6 +85,7 @@ const createGraphQLClient = ( const envName = hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER'; const hostHeader = process.env[envName]; + if (hostHeader) { headers.host = hostHeader; } @@ -102,16 +105,19 @@ export const sendEmailLink = async ( if (!params.invite_token || !params.sender_id) { return { missing: 'invite_token_or_sender_id' }; } + return null; case 'forgot_password': if (!params.user_id || !params.reset_token) { return { missing: 'user_id_or_reset_token' }; } + return null; case 'email_verification': if (!params.email_id || !params.verification_token) { return { missing: 'email_id_or_verification_token' }; } + return null; default: return { missing: 'email_type' }; @@ -126,6 +132,7 @@ export const sendEmailLink = async ( } const typeValidation = validateForType(); + if (typeValidation) { return typeValidation; } @@ -135,6 +142,7 @@ export const sendEmailLink = async ( }); const site = databaseInfo?.database?.sites?.nodes?.[0]; + if (!site) { throw new Error('Site not found for database'); } @@ -198,6 +206,7 @@ export const sendEmailLink = async ( const inviter = await client.request(GetUser, { userId: params.sender_id }); + inviterName = inviter?.user?.displayName; if (inviterName) { @@ -269,7 +278,7 @@ export const sendEmailLink = async ( }); if (isDryRun) { - // eslint-disable-next-line no-console + console.log('[send-email-link] DRY RUN email (skipping send)', { email_type: params.email_type, email: params.email, @@ -297,6 +306,7 @@ app.post('*', async (req: any, res: any, next: any) => { const databaseId = req.get('X-Database-Id') || req.get('x-database-id') || process.env.DEFAULT_DATABASE_ID; + if (!databaseId) { return res.status(400).json({ error: 'Missing X-Database-Id header or DEFAULT_DATABASE_ID' }); } @@ -324,9 +334,10 @@ export default app; // When executed directly (e.g. via `node dist/index.js`), start an HTTP server. if (require.main === module) { const port = Number(process.env.PORT ?? 8080); + // @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app (app as any).listen(port, () => { - // eslint-disable-next-line no-console + console.log(`[send-email-link] listening on port ${port}`); }); } diff --git a/functions/simple-email/src/index.ts b/functions/simple-email/src/index.ts index fdafb116f..e07ced6ff 100644 --- a/functions/simple-email/src/index.ts +++ b/functions/simple-email/src/index.ts @@ -1,6 +1,6 @@ import app from '@constructive-io/knative-job-fn'; -import { parseEnvBoolean } from '@pgpmjs/env'; import { send as sendEmail } from '@launchql/postmaster'; +import { parseEnvBoolean } from '@pgpmjs/env'; type SimpleEmailPayload = { to: string; @@ -19,9 +19,11 @@ const getRequiredField = ( field: keyof SimpleEmailPayload ) => { const value = payload[field]; + if (!isNonEmptyString(value)) { throw new Error(`Missing required field '${String(field)}'`); } + return value; }; @@ -62,7 +64,7 @@ app.post('*', async (req: any, res: any, next: any) => { }; if (isDryRun) { - // eslint-disable-next-line no-console + console.log('[simple-email] DRY RUN email (no send)', logContext); } else { // Send via the Postmaster package (Mailgun or configured provider) @@ -75,7 +77,7 @@ app.post('*', async (req: any, res: any, next: any) => { ...(replyTo && { replyTo }) }); - // eslint-disable-next-line no-console + console.log('[simple-email] Sent email', logContext); } @@ -91,9 +93,10 @@ export default app; // start an HTTP server on the provided PORT (default 8080). if (require.main === module) { const port = Number(process.env.PORT ?? 8080); + // @constructive-io/knative-job-fn exposes a .listen method that delegates to the underlying Express app (app as any).listen(port, () => { - // eslint-disable-next-line no-console + console.log(`[simple-email] listening on port ${port}`); }); } diff --git a/graphile/graphile-i18n/__tests__/plugin.test.ts b/graphile/graphile-i18n/__tests__/plugin.test.ts index 89d756105..6b6f9e8ff 100644 --- a/graphile/graphile-i18n/__tests__/plugin.test.ts +++ b/graphile/graphile-i18n/__tests__/plugin.test.ts @@ -1,11 +1,13 @@ import '../test-utils/env'; -import { join } from 'path'; + +import type { GraphQLQueryFn } from 'graphile-test'; import { getConnections, snapshot } from 'graphile-test'; +import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import type { GraphQLQueryFn } from 'graphile-test'; -import { GetProjectsAndLanguages } from '../test-utils/queries'; + import LangPlugin, { additionalGraphQLContextFromRequest } from '../src'; +import { GetProjectsAndLanguages } from '../test-utils/queries'; const SCHEMA = process.env.SCHEMA ?? 'app_public'; const sql = (f: string) => join(__dirname, '../sql', f); @@ -57,5 +59,6 @@ it('exposes localized strings', async () => { 'accept-language': 'es' } }); + expect(snapshot(data)).toMatchSnapshot(); }); diff --git a/graphile/graphile-i18n/src/env.ts b/graphile/graphile-i18n/src/env.ts index efc74a8fb..2887d841f 100644 --- a/graphile/graphile-i18n/src/env.ts +++ b/graphile/graphile-i18n/src/env.ts @@ -4,6 +4,7 @@ const commaSeparatedArray = makeValidator((value) => { if (typeof value !== 'string') { throw new Error('Expected a comma separated string'); } + return value .split(',') .map((entry) => entry.trim()) diff --git a/graphile/graphile-i18n/src/index.ts b/graphile/graphile-i18n/src/index.ts index 3f7d2f2a9..5df713509 100644 --- a/graphile/graphile-i18n/src/index.ts +++ b/graphile/graphile-i18n/src/index.ts @@ -1,13 +1,12 @@ import LangPlugin from './plugin'; -export { LangPlugin, type LangPluginOptions } from './plugin'; +export { env } from './env'; export { additionalGraphQLContextFromRequest, - makeLanguageDataLoaderForTable, type I18nGraphQLContext, type I18nRequestLike, - type LanguageDataLoaderFactory -} from './middleware'; -export { env } from './env'; + type LanguageDataLoaderFactory, + makeLanguageDataLoaderForTable} from './middleware'; +export { LangPlugin, type LangPluginOptions } from './plugin'; export default LangPlugin; diff --git a/graphile/graphile-i18n/src/middleware.ts b/graphile/graphile-i18n/src/middleware.ts index 4039a9b51..1c725fecd 100644 --- a/graphile/graphile-i18n/src/middleware.ts +++ b/graphile/graphile-i18n/src/middleware.ts @@ -1,7 +1,7 @@ +import langParser from 'accept-language-parser'; import DataLoader from 'dataloader'; import type { IncomingHttpHeaders } from 'http'; import type { PoolClient } from 'pg'; -import langParser from 'accept-language-parser'; import env from './env'; @@ -85,6 +85,7 @@ export const makeLanguageDataLoaderForTable = ( `, [ids, languageCodes] ); + return ids.map((id) => rows.find((row: LanguageRow) => row?.[identifier] === id) ); diff --git a/graphile/graphile-i18n/src/plugin.ts b/graphile/graphile-i18n/src/plugin.ts index e2c8767c2..eedc80d08 100644 --- a/graphile/graphile-i18n/src/plugin.ts +++ b/graphile/graphile-i18n/src/plugin.ts @@ -9,7 +9,6 @@ import type QueryBuilder from 'graphile-build-pg/node8plus/QueryBuilder'; import type { GraphQLFieldConfigMap } from 'graphql'; import { - additionalGraphQLContextFromRequest, makeLanguageDataLoaderForTable } from './middleware'; @@ -59,6 +58,7 @@ const hasI18nTag = (table: PgClass): boolean => const getPrimaryKeyInfo = (table: PgClass): KeyInfo => { const identifier = table.primaryKeyConstraint?.keyAttributes?.[0]?.name; const idType = table.primaryKeyConstraint?.keyAttributes?.[0]?.type?.name; + return { identifier, idType }; }; @@ -79,9 +79,11 @@ export const LangPlugin: Plugin = (builder, options) => { const tablesWithLanguageTablesIdInfo = tablesWithLanguageTables.reduce>( (memo: Record, table: PgClass) => { const keyInfo = getPrimaryKeyInfo(table); + if (table.tags.i18n) { memo[table.tags.i18n as string] = keyInfo; } + return memo; }, {} @@ -101,6 +103,7 @@ export const LangPlugin: Plugin = (builder, options) => { tablesWithLanguageTables.forEach((table: PgClass) => { const i18nTableName = table.tags.i18n as string; + i18nTables[i18nTableName] = { table: table.name, key: null, @@ -128,6 +131,7 @@ export const LangPlugin: Plugin = (builder, options) => { const foreignKeyConstraint: PgConstraint | undefined = foreignConstraintsThatMatter[0]; + if (!foreignKeyConstraint || foreignKeyConstraint.keyAttributes.length !== 1) { return; } @@ -180,6 +184,7 @@ export const LangPlugin: Plugin = (builder, options) => { } const variationsTableName = tables[table.name]; + if (!variationsTableName) { return fields; } @@ -204,6 +209,7 @@ export const LangPlugin: Plugin = (builder, options) => { : GraphQLString, description: `Locale for ${field}` }; + return memo; }, { @@ -226,6 +232,7 @@ export const LangPlugin: Plugin = (builder, options) => { const { addDataGenerator } = fieldContext as { addDataGenerator: (generator: () => { pgQuery: (queryBuilder: QueryBuilder) => void }) => void; }; + addDataGenerator(() => ({ pgQuery: (queryBuilder: QueryBuilder) => { queryBuilder.select( @@ -241,6 +248,7 @@ export const LangPlugin: Plugin = (builder, options) => { const columnName = i18nFields[field].attr; const escColumnName = escapeIdentifier(columnName); const escFieldName = escapeIdentifier(field); + return `coalesce(v.${escColumnName}, b.${escColumnName}) as ${escFieldName}`; }); diff --git a/graphile/graphile-many-to-many/__tests__/queries.test.ts b/graphile/graphile-many-to-many/__tests__/queries.test.ts index 70eeb2ed8..96f8c54d0 100644 --- a/graphile/graphile-many-to-many/__tests__/queries.test.ts +++ b/graphile/graphile-many-to-many/__tests__/queries.test.ts @@ -1,9 +1,9 @@ import '../test-utils/env'; import { existsSync, promises as fs, readdirSync } from 'fs'; -import { join } from 'path'; -import { getConnections } from 'graphile-test'; import type { GraphQLQueryFn } from 'graphile-test'; +import { getConnections } from 'graphile-test'; +import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; @@ -26,8 +26,10 @@ const getFixtureSets = (): FixtureSet[] => { return schemas .map((schema) => { const fixtureDir = join(SCHEMA_DIR, schema, 'fixtures/queries'); + try { const fixtures = readdirSync(fixtureDir).filter((file) => file.endsWith('.graphql')); + return fixtures.length ? { schema, fixtures } : null; } catch { return null; @@ -40,6 +42,7 @@ const fixtureSets = getFixtureSets(); const grantAuthenticatedAccess = async (pg: PgTestClient, schemaName: string) => { const ident = `"${schemaName.replace(/"/g, '""')}"`; + await pg.query( ` grant usage on schema ${ident} to authenticated; @@ -69,6 +72,7 @@ const getSchemaConnections = async (schemaName: string) => { ); const { db, pg, query, teardown } = connections; + return { db, pg, query, teardown }; }; @@ -82,6 +86,7 @@ describe.each(fixtureSets.map(({ schema, fixtures }) => [schema, fixtures] as co beforeAll(async () => { const connections = await getSchemaConnections(schema); + ({ db, pg, query, teardown } = connections); await grantAuthenticatedAccess(pg, schema); }); @@ -102,11 +107,12 @@ describe.each(fixtureSets.map(({ schema, fixtures }) => [schema, fixtures] as co const queryText = await fs.readFile(join(SCHEMA_DIR, schema, 'fixtures/queries', fixture), 'utf8'); const result = await query(queryText); const normalizedResult = JSON.parse(JSON.stringify(result)); + if (normalizedResult.errors) { // surface underlying errors in case snapshots hide details - /* eslint-disable no-console */ + console.log(normalizedResult.errors.map((error: any) => error.originalError ?? error)); - /* eslint-enable no-console */ + } expect(normalizedResult).toMatchSnapshot(); }); diff --git a/graphile/graphile-many-to-many/__tests__/schema.test.ts b/graphile/graphile-many-to-many/__tests__/schema.test.ts index d842d7d75..02406db01 100644 --- a/graphile/graphile-many-to-many/__tests__/schema.test.ts +++ b/graphile/graphile-many-to-many/__tests__/schema.test.ts @@ -1,15 +1,15 @@ import '../test-utils/env'; import { existsSync } from 'fs'; -import { join } from 'path'; -import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql'; -import { lexicographicSortSchema } from 'graphql/utilities'; import { PgConnectionArgCondition } from 'graphile-build-pg'; -import type { PostGraphileOptions } from 'postgraphile'; -import { getConnections } from 'graphile-test'; import type { GraphQLQueryFn } from 'graphile-test'; +import { getConnections } from 'graphile-test'; +import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql'; +import { lexicographicSortSchema } from 'graphql/utilities'; +import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; +import type { PostGraphileOptions } from 'postgraphile'; import PgManyToManyPlugin from '../src'; @@ -42,6 +42,7 @@ jest.setTimeout(20_000); const grantAuthenticatedAccess = async (pg: PgTestClient, schemaName: string) => { const ident = `"${schemaName.replace(/"/g, '""')}"`; + await pg.query( ` grant usage on schema ${ident} to authenticated; @@ -75,6 +76,7 @@ const getSchemaConnections = async ( ); const { db, pg, query, teardown } = connections; + return { db, pg, query, teardown }; }; @@ -86,6 +88,7 @@ describe.each(schemaTests)('$name', ({ schema, options }) => { beforeAll(async () => { const connections = await getSchemaConnections(schema, options); + ({ db, pg, query, teardown } = connections); await grantAuthenticatedAccess(pg, schema); }); @@ -104,11 +107,13 @@ describe.each(schemaTests)('$name', ({ schema, options }) => { it('prints schema', async () => { const result = await query(getIntrospectionQuery()); + if (result.errors?.length) { throw new Error(JSON.stringify(result.errors, null, 2)); } const schemaResult = buildClientSchema(result.data as any); const printed = printSchema(lexicographicSortSchema(schemaResult)); + expect(printed).toMatchSnapshot(); }); }); diff --git a/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeColumnsPlugin.ts b/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeColumnsPlugin.ts index 8a6113be6..cc412c266 100644 --- a/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeColumnsPlugin.ts +++ b/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeColumnsPlugin.ts @@ -129,6 +129,7 @@ const PgManyToManyRelationEdgeColumnsPlugin: Plugin = (builder) => { } }; }); + return { description: attr.description, type: nullableIf( diff --git a/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeTablePlugin.ts b/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeTablePlugin.ts index 3e64a311f..d77be5bbd 100644 --- a/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeTablePlugin.ts +++ b/graphile/graphile-many-to-many/src/PgManyToManyRelationEdgeTablePlugin.ts @@ -69,6 +69,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti fieldWithHooks, Self } = context; + if (!isPgManyToManyEdgeType || !pgManyToManyRelationship) { return fields; } @@ -92,6 +93,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti junctionTable.type.id, null ) as GraphQLNamedType | null; + if (!JunctionTableType) { throw new Error(`Could not determine type for table with id ${junctionTable.type.id}`); } @@ -109,6 +111,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti junctionRightConstraint ); const Type = isConnection ? JunctionTableConnectionType : JunctionTableType; + if (!Type) { return undefined; } @@ -124,6 +127,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti withPaginationAsFields: false, asJsonAggregate: !isConnection }; + addDataGenerator((parsedResolveInfoFragment: any) => { return { pgQuery: (queryBuilder: any) => { @@ -143,6 +147,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti const junctionPrimaryKeyConstraint = junctionTable.primaryKeyConstraint; const junctionPrimaryKeyAttributes = junctionPrimaryKeyConstraint && junctionPrimaryKeyConstraint.keyAttributes; + if (junctionPrimaryKeyAttributes) { innerQueryBuilder.beforeLock('orderBy', () => { // append order by primary key to the list of orders @@ -178,6 +183,7 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti queryBuilder.context, queryBuilder.rootValue ); + return sql.fragment`(${query})`; }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); } @@ -192,9 +198,11 @@ const PgManyToManyRelationEdgeTablePlugin: Plugin = (builder, { pgSimpleCollecti args: {}, resolve: (data: any, _args: any, _context: any, resolveInfo: GraphQLResolveInfo) => { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); + if (isConnection) { return addStartEndCursor(data[safeAlias]); } + return data[safeAlias]; } }; diff --git a/graphile/graphile-many-to-many/src/PgManyToManyRelationInflectionPlugin.ts b/graphile/graphile-many-to-many/src/PgManyToManyRelationInflectionPlugin.ts index 376ad1da9..badc85ad4 100644 --- a/graphile/graphile-many-to-many/src/PgManyToManyRelationInflectionPlugin.ts +++ b/graphile/graphile-many-to-many/src/PgManyToManyRelationInflectionPlugin.ts @@ -29,6 +29,7 @@ const PgManyToManyRelationInflectionPlugin: Plugin = (builder) => { if (junctionRightConstraint.tags.manyToManyFieldName) { return junctionRightConstraint.tags.manyToManyFieldName; } + return this.camelCase( `${this.pluralize(this._singularizedTableName(rightTable))}-by-${this._singularizedTableName( junctionTable @@ -52,6 +53,7 @@ const PgManyToManyRelationInflectionPlugin: Plugin = (builder) => { if (junctionRightConstraint.tags.manyToManySimpleFieldName) { return junctionRightConstraint.tags.manyToManySimpleFieldName; } + return this.camelCase( `${this.pluralize(this._singularizedTableName(rightTable))}-by-${this._singularizedTableName( junctionTable @@ -83,6 +85,7 @@ const PgManyToManyRelationInflectionPlugin: Plugin = (builder) => { junctionLeftConstraint, junctionRightConstraint ); + return this.upperCamelCase(`${leftTableTypeName}-${relationName}-many-to-many-edge`); }; @@ -109,6 +112,7 @@ const PgManyToManyRelationInflectionPlugin: Plugin = (builder) => { junctionRightConstraint, leftTableTypeName ); + return this.upperCamelCase(`${leftTableTypeName}-${relationName}-many-to-many-connection`); }; diff --git a/graphile/graphile-many-to-many/src/PgManyToManyRelationPlugin.ts b/graphile/graphile-many-to-many/src/PgManyToManyRelationPlugin.ts index 66ad3415c..b65611581 100644 --- a/graphile/graphile-many-to-many/src/PgManyToManyRelationPlugin.ts +++ b/graphile/graphile-many-to-many/src/PgManyToManyRelationPlugin.ts @@ -1,5 +1,4 @@ import type { Plugin } from 'graphile-build'; -import type { PgClass } from 'graphile-build-pg'; import type { GraphQLFieldConfigMap, GraphQLNamedType, @@ -33,6 +32,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption fieldWithHooks, Self } = context; + if (!isPgRowType || !leftTable || leftTable.kind !== 'class') { return fields; } @@ -54,6 +54,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption rightTable.type.id, null ) as GraphQLNamedType | null; + if (!RightTableType) { throw new Error(`Could not determine type for table with id ${rightTable.type.id}`); } @@ -114,6 +115,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption withPaginationAsFields: false, asJsonAggregate: !isConnection }; + addDataGenerator((parsedResolveInfoFragment: any) => { return { pgQuery: (queryBuilder: any) => { @@ -134,6 +136,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption const rightPrimaryKeyConstraint = rightTable.primaryKeyConstraint; const rightPrimaryKeyAttributes = rightPrimaryKeyConstraint && rightPrimaryKeyConstraint.keyAttributes; + if (rightPrimaryKeyAttributes) { innerQueryBuilder.beforeLock('orderBy', () => { // append order by primary key to the list of orders @@ -167,6 +170,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption sql.identifier(junctionTable.namespace.name, junctionTable.name), sql.identifier(junctionRightKeyAttribute.name) ); + subqueryBuilder.where( sql.fragment`${sql.identifier( junctionLeftKeyAttribute.name @@ -183,6 +187,7 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption queryBuilder.context, queryBuilder.rootValue ); + return sql.fragment`(${query})`; }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); } @@ -197,9 +202,11 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption args: {}, resolve: (data: any, _args: any, _context: any, resolveInfo: GraphQLResolveInfo) => { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); + if (isConnection) { return addStartEndCursor(data[safeAlias]); } + return data[safeAlias]; } }; @@ -227,12 +234,14 @@ const PgManyToManyRelationPlugin: Plugin = (builder, options: PgManyToManyOption pgSimpleCollections; const hasConnections = simpleCollections !== 'only'; const hasSimpleCollections = simpleCollections === 'only' || simpleCollections === 'both'; + if (hasConnections) { makeFields(true); } if (hasSimpleCollections) { makeFields(false); } + return memoWithRelations; }, {} diff --git a/graphile/graphile-many-to-many/src/createManyToManyConnectionType.ts b/graphile/graphile-many-to-many/src/createManyToManyConnectionType.ts index 5bc808bce..362e9e3a7 100644 --- a/graphile/graphile-many-to-many/src/createManyToManyConnectionType.ts +++ b/graphile/graphile-many-to-many/src/createManyToManyConnectionType.ts @@ -2,10 +2,10 @@ import type { Build } from 'graphile-build'; import type { PgClass } from 'graphile-build-pg'; import type { GraphQLFieldConfigMap, - GraphQLObjectType, - GraphQLResolveInfo, GraphQLNamedType, GraphQLNullableType, + GraphQLObjectType, + GraphQLResolveInfo, GraphQLType } from 'graphql'; @@ -33,6 +33,7 @@ const hasNonNullKey = (row: Record): boolean => { } } } + return false; }; @@ -74,6 +75,7 @@ const createManyToManyConnectionType = ( if ((identifiers && hasNonNullKey(identifiers)) || hasNonNullKey(row)) { return row; } + return null; }; @@ -81,6 +83,7 @@ const createManyToManyConnectionType = ( leftTable.type.id, null ) as GraphQLNamedType | null; + if (!LeftTableType) { throw new Error(`Could not determine type for table with id ${leftTable.type.id}`); } @@ -89,6 +92,7 @@ const createManyToManyConnectionType = ( rightTable.type.id, null ) as GraphQLNamedType | null; + if (!TableType) { throw new Error(`Could not determine type for table with id ${rightTable.type.id}`); } @@ -127,6 +131,7 @@ const createManyToManyConnectionType = ( } } })); + return { description: 'A cursor for use in pagination.', type: Cursor, @@ -149,6 +154,7 @@ const createManyToManyConnectionType = ( resolve(data: any, _args: any, _context: any, resolveInfo: GraphQLResolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const record = handleNullRow(data[safeAlias], data.__identifiers); + return record; } }, @@ -195,6 +201,7 @@ const createManyToManyConnectionType = ( fieldWithHooks: any; }): GraphQLFieldConfigMap => { recurseDataGeneratorsForField('pageInfo', true); + return { nodes: pgField( build, @@ -207,8 +214,10 @@ const createManyToManyConnectionType = ( ), resolve(data: any, _args: any, _context: any, resolveInfo: GraphQLResolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); + return data.data.map((entry: any) => { const record = handleNullRow(entry[safeAlias], entry[safeAlias].__identifiers); + return record; }); } @@ -226,6 +235,7 @@ const createManyToManyConnectionType = ( type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EdgeType))), resolve(data: any, _args: any, _context: any, resolveInfo: GraphQLResolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); + return data.data.map((entry: any) => ({ ...entry, ...entry[safeAlias] diff --git a/graphile/graphile-many-to-many/src/index.ts b/graphile/graphile-many-to-many/src/index.ts index 3dd6564a0..0728d0966 100644 --- a/graphile/graphile-many-to-many/src/index.ts +++ b/graphile/graphile-many-to-many/src/index.ts @@ -26,6 +26,7 @@ const PgManyToManyPlugin: Plugin = (builder: any, options: PgManyToManyOptions = ); } }; + depends('graphile-build-pg', '^4.5.0'); // Register this plugin diff --git a/graphile/graphile-many-to-many/src/manyToManyRelationships.ts b/graphile/graphile-many-to-many/src/manyToManyRelationships.ts index 905c3c512..621573da7 100644 --- a/graphile/graphile-many-to-many/src/manyToManyRelationships.ts +++ b/graphile/graphile-many-to-many/src/manyToManyRelationships.ts @@ -26,6 +26,7 @@ const manyToManyRelationships = (leftTable: PgClass, build: any): ManyToManyRela } const junctionTable = introspectionResultsByKind.classById[junctionLeftConstraint.classId]; + if (!junctionTable) { throw new Error( `Could not find the table that referenced us (constraint: ${junctionLeftConstraint.name})` @@ -44,6 +45,7 @@ const manyToManyRelationships = (leftTable: PgClass, build: any): ManyToManyRela ) .reduce((memoRightInner, junctionRightConstraint: PgConstraint) => { const rightTable = junctionRightConstraint.foreignClass; + if (omit(rightTable, 'read') || omit(rightTable, 'manyToMany')) { return memoRightInner; } @@ -86,6 +88,7 @@ const manyToManyRelationships = (leftTable: PgClass, build: any): ManyToManyRela const junctionRightConstraintIsUnique = !!junctionTable.constraints.find((c: PgConstraint) => isUniqueConstraint(c as any, junctionRightKeyAttributes) ); + if (junctionLeftConstraintIsUnique || junctionRightConstraintIsUnique) { return memoRightInner; } @@ -117,6 +120,7 @@ const manyToManyRelationships = (leftTable: PgClass, build: any): ManyToManyRela } ]; }, []); + return [...memoLeft, ...memoRight]; }, []); }; diff --git a/graphile/graphile-meta-schema/__tests__/index.test.ts b/graphile/graphile-meta-schema/__tests__/index.test.ts index b6a0ea7c4..f1a7a32b5 100644 --- a/graphile/graphile-meta-schema/__tests__/index.test.ts +++ b/graphile/graphile-meta-schema/__tests__/index.test.ts @@ -1,9 +1,11 @@ import '../test-utils/env'; + import PgManyToMany from '@graphile-contrib/pg-many-to-many'; +import { getConnections, type GraphQLQueryFn,seed, snapshot } from 'graphile-test'; import { join } from 'path'; -import { getConnections, snapshot, seed, type GraphQLQueryFn } from 'graphile-test'; import type { PgTestClient } from 'pgsql-test/test-client'; +import { PgMetaschemaPlugin } from '../src'; import { GetBelongsToRelations, GetHasManyRelations, @@ -13,7 +15,6 @@ import { GetMetaSchema, GetMetaSchemaUnion } from '../test-utils/queries'; -import { PgMetaschemaPlugin } from '../src'; const SCHEMA = process.env.SCHEMA ?? 'app_meta'; const sql = (file: string) => join(__dirname, '../sql', file); @@ -56,47 +57,55 @@ afterAll(async () => { const expectNoErrors = (result: unknown): void => { const data = result as { errors?: unknown }; + expect(data.errors).toBeUndefined(); }; it('GetMetaSchema', async () => { const data = await query(GetMetaSchema); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetMetaSchemaUnion', async () => { const data = await query(GetMetaSchemaUnion); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetMetaInflection', async () => { const data = await query(GetMetaInflection); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetHasOneRelations', async () => { const data = await query(GetHasOneRelations); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetHasManyRelations', async () => { const data = await query(GetHasManyRelations); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetBelongsToRelations', async () => { const data = await query(GetBelongsToRelations); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); it('GetManyToManyRelations', async () => { const data = await query(GetManyToManyRelations); + expectNoErrors(data); expect(snapshot(data)).toMatchSnapshot(); }); diff --git a/graphile/graphile-meta-schema/src/index.ts b/graphile/graphile-meta-schema/src/index.ts index 91ea9de4e..17d17fde0 100644 --- a/graphile/graphile-meta-schema/src/index.ts +++ b/graphile/graphile-meta-schema/src/index.ts @@ -221,17 +221,20 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( if (type.isPgArray && type.arrayItemType?.name) { return type.arrayItemType.name; } + return type.name; }, pgAlias(type: PgType): string { if (type.isPgArray && type.arrayItemType?.name) { return aliasTypes(type.arrayItemType.name); } + return aliasTypes(type.name); }, gqlType(type: PgType): string { const gqlType = pgGetGqlTypeByTypeIdAndModifier(type.id, type.attrTypeModifier ?? null); const typeName = getTypeName(gqlType); + switch (typeName) { case 'GeometryInterface': case 'GeometryPoint': @@ -244,6 +247,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( subtype(type: PgType): string | null { const gqlType = pgGetGqlTypeByTypeIdAndModifier(type.id, type.attrTypeModifier ?? null); const typeName = getTypeName(gqlType); + switch (typeName) { case 'GeometryInterface': case 'GeometryPoint': @@ -255,6 +259,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( }, typmod(type: PgType): Record | null { const modifier = type.attrTypeModifier; + if (!modifier) return null; if (type.name === 'geography' || type.name === 'geometry') { @@ -262,6 +267,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( const subtype = (modifier & 0x000000fc) >> 2; const hasZ = ((modifier & 0x00000002) >> 1) === 1; const hasM = (modifier & 0x00000001) === 1; + if (subtype < GIS_TYPES.length) { return { srid, @@ -272,6 +278,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( }; } } + return { modifier }; }, modifier(type: PgType): number | null | undefined { @@ -292,12 +299,14 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( attrTypeModifier: attr.typeModifier }; } + return attr.type; } }, MetaschemaTableInflection: { deleteByPrimaryKey(table: PgClass): string | null { if (!table.primaryKeyConstraint?.keyAttributes?.length) return null; + return inflection.deleteByKeys( table.primaryKeyConstraint.keyAttributes, table, @@ -306,6 +315,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( }, updateByPrimaryKey(table: PgClass): string | null { if (!table.primaryKeyConstraint?.keyAttributes?.length) return null; + return inflection.updateByKeys( table.primaryKeyConstraint.keyAttributes, table, @@ -337,6 +347,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( if (typeof inflection.filterType === 'function') { return inflection.filterType(inflection.tableType(table)) ?? null; } + return null; }, inputType(table: PgClass): string { @@ -379,6 +390,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( MetaschemaTableQuery: { delete(table: PgClass): string | null { if (!table.primaryKeyConstraint?.keyAttributes?.length) return null; + return inflection.deleteByKeys( table.primaryKeyConstraint.keyAttributes, table, @@ -387,6 +399,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( }, update(table: PgClass): string | null { if (!table.primaryKeyConstraint?.keyAttributes?.length) return null; + return inflection.updateByKeys( table.primaryKeyConstraint.keyAttributes, table, @@ -468,6 +481,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( junctionLeftConstraint, junctionRightConstraint } = relation; + return inflection.manyToManyRelationByKeys( leftKeyAttributes, junctionLeftKeyAttributes, @@ -538,6 +552,7 @@ const PgMetaschemaPlugin: Plugin = makeExtendSchemaPlugin( return introspection.class.filter((table) => { if (!schemas.includes(table.namespaceName)) return false; if (table.classKind !== 'r') return false; + return true; }); } diff --git a/graphile/graphile-pg-type-mappings/__tests__/plugin.test.ts b/graphile/graphile-pg-type-mappings/__tests__/plugin.test.ts index 064d04eac..475b977a1 100644 --- a/graphile/graphile-pg-type-mappings/__tests__/plugin.test.ts +++ b/graphile/graphile-pg-type-mappings/__tests__/plugin.test.ts @@ -1,8 +1,9 @@ -import { join } from 'path'; import { getConnections, GraphQLQueryFn } from 'graphile-test'; +import gql from 'graphql-tag'; +import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import gql from 'graphql-tag'; + import CustomPgTypeMappingsPlugin from '../src'; const SCHEMA = 'public'; @@ -65,6 +66,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should map default types correctly', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -118,6 +120,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should handle custom type mappings', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -169,6 +172,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should map multiple custom types correctly', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -220,6 +224,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should override multiple default mappings', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -274,6 +279,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should handle both adding new types and overriding defaults', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -320,6 +326,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should use only default mappings when custom mappings is empty array', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); @@ -372,6 +379,7 @@ describe('CustomPgTypeMappingsPlugin', () => { it('should handle plugin factory with options', async () => { const typeRes = await query(TYPE_INTROSPECTION_QUERY); + expect(typeRes).toMatchSnapshot(); }); }); diff --git a/graphile/graphile-pg-type-mappings/src/index.ts b/graphile/graphile-pg-type-mappings/src/index.ts index 564cb12bf..351502d19 100644 --- a/graphile/graphile-pg-type-mappings/src/index.ts +++ b/graphile/graphile-pg-type-mappings/src/index.ts @@ -35,6 +35,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // First add defaults for (const mapping of DEFAULT_MAPPINGS) { const key = `${mapping.namespaceName}.${mapping.name}`; + if (!seen.has(key)) { allMappings.push(mapping); seen.add(key); @@ -44,11 +45,13 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // Then add/override with custom mappings for (const mapping of customMappings) { const key = `${mapping.namespaceName}.${mapping.name}`; + if (seen.has(key)) { // Override existing mapping const index = allMappings.findIndex( m => m.name === mapping.name && m.namespaceName === mapping.namespaceName ); + if (index !== -1) { allMappings[index] = mapping; } @@ -74,6 +77,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // This allows the plugin to work even when optional dependencies are missing // (e.g., GeoJSON requires PostGIS plugin) const gqlType = build.getTypeByName(type); + if (gqlType) { build.pgRegisterGqlTypeByTypeId(pgType.id, () => gqlType); // Store mapping for field resolver @@ -99,6 +103,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // Check if this field's type is in our mappings const mapping = typeMappingsByTypeId.get(attr.type.id); + if (!mapping) { return field; } @@ -112,6 +117,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // Get the composite type's attributes // @ts-ignore const compositeType = attr.type.class; + if (!compositeType || !compositeType.attributes) { return field; } @@ -120,6 +126,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { const { resolve: oldResolve, ...rest } = field; const defaultResolver = (obj: Record) => { const value = oldResolve ? oldResolve(obj) : (fieldName ? obj[fieldName] : undefined); + if (value == null) { return null; } @@ -132,6 +139,7 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // For composite types mapped to scalars, extract the first field's value // This handles types like email(value text) -> extract value const attributes = compositeType.attributes; + if (attributes && attributes.length > 0) { // Try to extract the first attribute's value // PostgreSQL composite types are returned as objects with attribute names as keys @@ -146,12 +154,14 @@ const CustomPgTypeMappingsPlugin: Plugin = (builder, options) => { // Try camelCase version (PostGraphile might convert field names) const camelCaseName = attrFieldName.replace(/_([a-z])/g, (_: string, letter: string) => letter.toUpperCase()); + if (value[camelCaseName] !== undefined) { return value[camelCaseName]; } // Fallback: return the first property value const firstKey = Object.keys(value)[0]; + if (firstKey) { return value[firstKey]; } diff --git a/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/queries.test.ts b/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/queries.test.ts index e7d79cd3e..4d5abdde1 100644 --- a/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/queries.test.ts +++ b/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/queries.test.ts @@ -1,11 +1,11 @@ import { readdirSync } from 'fs'; import { readFile } from 'fs/promises'; -import { join } from 'path'; +import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; +import PostgisPlugin from 'graphile-postgis'; import type { GraphQLQueryFnObj } from 'graphile-test'; import { getConnectionsObject, seed, snapshot } from 'graphile-test'; +import { join } from 'path'; import type { PgTestClient } from 'pgsql-test/test-client'; -import PostgisPlugin from 'graphile-postgis'; -import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; import PostgisConnectionFilterPlugin from '../../src'; diff --git a/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/schema/defaultOptions.test.ts b/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/schema/defaultOptions.test.ts index d351b2eb2..9f9da86f3 100644 --- a/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/schema/defaultOptions.test.ts +++ b/graphile/graphile-plugin-connection-filter-postgis/__tests__/integration/schema/defaultOptions.test.ts @@ -1,13 +1,13 @@ +import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; +import PostgisPlugin from 'graphile-postgis'; import { join } from 'path'; import { Pool } from 'pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; -import PostgisPlugin from 'graphile-postgis'; -import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; import PostgisConnectionFilterPlugin from '../../../src'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +23,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +31,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter-postgis/src/PgConnectionArgFilterPostgisOperatorsPlugin.ts b/graphile/graphile-plugin-connection-filter-postgis/src/PgConnectionArgFilterPostgisOperatorsPlugin.ts index 02ed71427..f2247a78e 100644 --- a/graphile/graphile-plugin-connection-filter-postgis/src/PgConnectionArgFilterPostgisOperatorsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter-postgis/src/PgConnectionArgFilterPostgisOperatorsPlugin.ts @@ -98,6 +98,7 @@ const PgConnectionArgFilterPostgisOperatorsPlugin: Plugin = ( ); const subtypes = [0, 1, 2, 3, 4, 5, 6, 7]; + for (const subtype of subtypes) { for (const hasZ of [false, true]) { for (const hasM of [false, true]) { @@ -205,6 +206,7 @@ const PgConnectionArgFilterPostgisOperatorsPlugin: Plugin = ( pgGISExtension.namespaceName === "public" ? sql.identifier(fn.toLowerCase()) : sql.identifier(pgGISExtension.namespaceName, fn.toLowerCase()); + specs.push({ typeNames: gqlTypeNamesByGisBaseTypeName[ diff --git a/graphile/graphile-plugin-connection-filter-postgis/test-utils/printSchema.ts b/graphile/graphile-plugin-connection-filter-postgis/test-utils/printSchema.ts index 7043b8059..bbe1d267c 100644 --- a/graphile/graphile-plugin-connection-filter-postgis/test-utils/printSchema.ts +++ b/graphile/graphile-plugin-connection-filter-postgis/test-utils/printSchema.ts @@ -3,5 +3,6 @@ import { lexicographicSortSchema, printSchema } from "graphql/utilities"; export const printSchemaOrdered = (originalSchema: GraphQLSchema): string => { const schema = buildASTSchema(parse(printSchema(originalSchema))); + return printSchema(lexicographicSortSchema(schema)); }; diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/queries.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/queries.test.ts index 85ee9ffb7..f514001bd 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/queries.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/queries.test.ts @@ -2,11 +2,11 @@ import '../../test-utils/env'; import { readdirSync } from 'fs'; import { readFile } from 'fs/promises'; -import { join } from 'path'; import type { Plugin } from 'graphile-build'; import { PgConnectionArgCondition } from 'graphile-build-pg'; import type { GraphQLQueryFnObj } from 'graphile-test'; import { getConnectionsObject, seed, snapshot } from 'graphile-test'; +import { join } from 'path'; import type { PgTestClient } from 'pgsql-test/test-client'; import type { PostGraphileOptions } from 'postgraphile'; @@ -139,6 +139,7 @@ const contexts: Partial> = {}; beforeAll(async () => { for (const variant of Object.keys(variantConfigs) as ConnectionVariant[]) { const config = variantConfigs[variant]; + contexts[variant] = await createContext( config.overrideSettings, config.graphileBuildOptions @@ -177,6 +178,7 @@ describe.each(queryFileNames)('%s', (queryFileName) => { it('matches snapshot', async () => { const query = await readFile(join(queriesDir, queryFileName), 'utf8'); const result = await ctx.query({ query }); + expect(snapshot(result)).toMatchSnapshot(); }); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/addConnectionFilterOperator.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/addConnectionFilterOperator.test.ts index 289a907ba..9a6b7527a 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/addConnectionFilterOperator.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/addConnectionFilterOperator.test.ts @@ -1,16 +1,17 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; -import CustomOperatorsPlugin from '../../../test-utils/customOperatorsPlugin'; import ConnectionFilterPlugin from '../../../src/index'; +import CustomOperatorsPlugin from '../../../test-utils/customOperatorsPlugin'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; const SCHEMA = process.env.SCHEMA ?? 'p'; @@ -24,6 +25,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -31,6 +33,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedFieldTypes.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedFieldTypes.test.ts index 011147945..0536bc213 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedFieldTypes.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedFieldTypes.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedOperators.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedOperators.test.ts index 1fdbb0d03..9a41c184e 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedOperators.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/allowedOperators.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/arraysFalse.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/arraysFalse.test.ts index 7754581e2..9bea82974 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/arraysFalse.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/arraysFalse.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/computedColumnsFalse.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/computedColumnsFalse.test.ts index b13972549..0235d732d 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/computedColumnsFalse.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/computedColumnsFalse.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/defaultOptions.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/defaultOptions.test.ts index 70848afd8..370a63c9e 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/defaultOptions.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/defaultOptions.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); @@ -49,5 +52,6 @@ it('prints a schema with the filter plugin', async () => { disableDefaultMutations: true, legacyRelations: 'omit', }); + expect(schema).toMatchSnapshot(); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/ignoreIndexesFalse.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/ignoreIndexesFalse.test.ts index e0dfbd01e..e8cfa3db7 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/ignoreIndexesFalse.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/ignoreIndexesFalse.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/logicalOperatorsFalse.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/logicalOperatorsFalse.test.ts index 6cc9ef6c8..eebe03b8f 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/logicalOperatorsFalse.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/logicalOperatorsFalse.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/operatorNames.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/operatorNames.test.ts index 094e62a7d..f2a075e49 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/operatorNames.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/operatorNames.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsTrue.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsTrue.test.ts index 2973e6f2c..3c82769a5 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsTrue.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsTrue.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsWithListInflectors.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsWithListInflectors.test.ts index 8fdd7ce5e..9bd613512 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsWithListInflectors.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/relationsWithListInflectors.test.ts @@ -1,14 +1,15 @@ import '../../../test-utils/env'; + +import PgSimplify from '@graphile-contrib/pg-simplify-inflector'; +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; -import PgSimplify from '@graphile-contrib/pg-simplify-inflector'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -24,6 +25,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -31,6 +33,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/setofFunctionsFalse.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/setofFunctionsFalse.test.ts index 729d2dfa0..12d07cf89 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/setofFunctionsFalse.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/setofFunctionsFalse.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/useCustomNetworkScalars.test.ts b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/useCustomNetworkScalars.test.ts index 8494802b9..a9fbae9c0 100644 --- a/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/useCustomNetworkScalars.test.ts +++ b/graphile/graphile-plugin-connection-filter/__tests__/integration/schema/useCustomNetworkScalars.test.ts @@ -1,13 +1,14 @@ import '../../../test-utils/env'; + +import { PgConnectionArgCondition } from 'graphile-build-pg'; import { join } from 'path'; import { Pool } from 'pg'; -import { PgConnectionArgCondition } from 'graphile-build-pg'; +import { getConnections, seed } from 'pgsql-test'; +import type { PgTestClient } from 'pgsql-test/test-client'; import { createPostGraphileSchema, type PostGraphileOptions, } from 'postgraphile'; -import { getConnections, seed } from 'pgsql-test'; -import type { PgTestClient } from 'pgsql-test/test-client'; import ConnectionFilterPlugin from '../../../src/index'; import { printSchemaOrdered } from '../../../test-utils/printSchema'; @@ -23,6 +24,7 @@ const createSchemaSnapshot = async ( options: PostGraphileOptions ): Promise => { const schema = await createPostGraphileSchema(pool, [SCHEMA], options); + return printSchemaOrdered(schema); }; @@ -30,6 +32,7 @@ beforeAll(async () => { const connections = await getConnections({}, [ seed.sqlfile([sql('schema.sql')]), ]); + ({ pg: db, teardown } = connections); pool = new Pool(db.config); }); diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterBackwardRelationsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterBackwardRelationsPlugin.ts index fceaf332f..9e25575d6 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterBackwardRelationsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterBackwardRelationsPlugin.ts @@ -15,6 +15,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( } = rawOptions as ConnectionFilterConfig; const hasConnections = pgSimpleCollections !== 'only'; const simpleInflectorsAreShorter = pgOmitListSuffix === true; + if ( simpleInflectorsAreShorter && connectionFilterUseListInflectors === undefined @@ -92,6 +93,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( } const foreignTable = introspectionResultsByKind.classById[foreignConstraint.classId]; + if (!foreignTable) { throw new Error( `Could not find the foreign table (constraint: ${foreignConstraint.name})` @@ -116,6 +118,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( const foreignKeyAttributes = foreignConstraint.keyAttributeNums.map( (num) => foreignAttributes.filter((attr) => attr.num === num)[0] ); + if (keyAttributes.some((attr) => omit(attr, 'read'))) { return memo; } @@ -133,6 +136,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( (n, i) => foreignKeyAttributes[i].num === n ) ); + memo.push({ table, keyAttributes, @@ -141,6 +145,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( foreignConstraint, isOneToMany: !isForeignKeyUnique, }); + return memo; }, []); @@ -219,6 +224,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( foreignTableFilterTypeName, queryBuilder ); + return sqlFragment == null ? null : sql.query`exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`; @@ -282,8 +288,10 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( null, { backwardRelationSpec } ); + return sqlFragment == null ? null : sqlFragment; }; + return resolveMany; }; @@ -303,6 +311,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( foreignTable, foreignTableTypeName ); + if (!ForeignTableFilterType) continue; if (isOneToMany) { @@ -311,6 +320,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( table, foreignTable ); + if (!connectionFilterTypesByTypeName[filterManyTypeName]) { connectionFilterTypesByTypeName[filterManyTypeName] = newWithHooks( GraphQLInputObjectType, @@ -341,6 +351,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( ); const filterFieldName = inflection.filterManyRelationByKeysFieldName(fieldName); + addField( filterFieldName, `Filter by the object’s \`${fieldName}\` relation.`, @@ -354,6 +365,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( const existsFieldName = inflection.filterBackwardManyRelationExistsFieldName(fieldName); + addField( existsFieldName, `Some related \`${fieldName}\` exist.`, @@ -374,6 +386,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( ); const filterFieldName = inflection.filterSingleRelationByKeysBackwardsFieldName(fieldName); + addField( filterFieldName, `Filter by the object’s \`${fieldName}\` relation.`, @@ -387,6 +400,7 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( const existsFieldName = inflection.filterBackwardSingleRelationExistsFieldName(fieldName); + addField( existsFieldName, `A related \`${fieldName}\` exists.`, @@ -502,13 +516,14 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = ( foreignTableFilterTypeName, queryBuilder ); + if (sqlFragment == null) { return null; - } else if (fieldName === 'every') { + } if (fieldName === 'every') { return sql.query`not exists(${sqlSelectWhereKeysMatch} and not (${sqlFragment}))`; - } else if (fieldName === 'some') { + } if (fieldName === 'some') { return sql.query`exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`; - } else if (fieldName === 'none') { + } if (fieldName === 'none') { return sql.query`not exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`; } throw new Error(`Unknown field name: ${fieldName}`); diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterColumnsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterColumnsPlugin.ts index a75e694b4..02510a6e2 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterColumnsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterColumnsPlugin.ts @@ -1,5 +1,6 @@ import type { Plugin } from 'graphile-build'; import type { PgAttribute } from 'graphile-build-pg'; + import { ConnectionFilterResolver } from './PgConnectionArgFilterPlugin'; const PgConnectionArgFilterColumnsPlugin: Plugin = (builder) => { @@ -35,7 +36,9 @@ const PgConnectionArgFilterColumnsPlugin: Plugin = (builder) => { .filter((attr) => !omit(attr, 'filter')) .reduce((memo: { [fieldName: string]: PgAttribute }, attr) => { const fieldName: string = inflection.column(attr); + memo[fieldName] = attr; + return memo; }, {}); @@ -48,10 +51,12 @@ const PgConnectionArgFilterColumnsPlugin: Plugin = (builder) => { attr.typeId, attr.typeModifier ); + if (!OperatorsType) { return memo; } operatorsTypeNameByFieldName[fieldName] = OperatorsType.name; + return extend(memo, { [fieldName]: fieldWithHooks( fieldName, diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterCompositeTypeColumnsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterCompositeTypeColumnsPlugin.ts index be8ca6495..652db4066 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterCompositeTypeColumnsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterCompositeTypeColumnsPlugin.ts @@ -10,6 +10,7 @@ const PgConnectionArgFilterCompositeTypeColumnsPlugin: Plugin = ( ) => { const { connectionFilterAllowedFieldTypes } = rawOptions as ConnectionFilterConfig; + builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => { const { extend, @@ -50,7 +51,9 @@ const PgConnectionArgFilterCompositeTypeColumnsPlugin: Plugin = ( ) // keep only the composite type columns .reduce((memo: { [fieldName: string]: PgAttribute }, attr) => { const fieldName: string = inflection.column(attr); + memo[fieldName] = attr; + return memo; }, {}); @@ -62,10 +65,12 @@ const PgConnectionArgFilterCompositeTypeColumnsPlugin: Plugin = ( attr.typeId, attr.typeModifier ); + if (!NodeType) { return memo; } const nodeTypeName = NodeType.name; + // Respect `connectionFilterAllowedFieldTypes` config option if ( connectionFilterAllowedFieldTypes && @@ -80,10 +85,12 @@ const PgConnectionArgFilterCompositeTypeColumnsPlugin: Plugin = ( attr.type.class, nodeTypeName ); + if (!CompositeFilterType) { return memo; } filterTypeNameByFieldName[fieldName] = filterTypeName; + return extend(memo, { [fieldName]: fieldWithHooks( fieldName, diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterComputedColumnsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterComputedColumnsPlugin.ts index 0c2c78322..86aa0b988 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterComputedColumnsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterComputedColumnsPlugin.ts @@ -10,6 +10,7 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( ) => { const { connectionFilterComputedColumns } = rawOptions as ConnectionFilterConfig; + builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => { const { extend, @@ -52,6 +53,7 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( table, proc ); + if (!computedColumnDetails) return memo; const { pseudoColumnName } = computedColumnDetails; @@ -63,6 +65,7 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( proc.argModes[idx] === 'b' // this arg is `inout` ).length; const nonOptionalArgumentsCount = inputArgsCount - proc.argDefaultsNum; + if (nonOptionalArgumentsCount > 1) { return memo; } @@ -72,10 +75,13 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( const returnType = introspectionResultsByKind.typeById[proc.returnTypeId]; const returnTypeTable = introspectionResultsByKind.classById[returnType.classId]; + if (returnTypeTable) return memo; const isRecordLike = returnType.id === '2249'; + if (isRecordLike) return memo; const isVoid = String(returnType.id) === '2278'; + if (isVoid) return memo; // Looks good @@ -84,7 +90,9 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( proc, table ); + memo = build.extend(memo, { [fieldName]: proc }); + return memo; }, {}); @@ -97,10 +105,12 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( proc.returnTypeId, null ); + if (!OperatorsType) { return memo; } operatorsTypeNameByFieldName[fieldName] = OperatorsType.name; + return extend(memo, { [fieldName]: fieldWithHooks( fieldName, @@ -166,8 +176,10 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( ) { prev.push(build.pgIntrospectionResultsByKind.typeById[typeId]); } + return prev; }, []); + if ( argTypes .slice(1) @@ -180,6 +192,7 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = ( } const pseudoColumnName = proc.name.substr(table.name.length + 1); + return { argTypes, pseudoColumnName }; } }; diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterForwardRelationsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterForwardRelationsPlugin.ts index 789257563..666b2a985 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterForwardRelationsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterForwardRelationsPlugin.ts @@ -51,6 +51,7 @@ const PgConnectionArgFilterForwardRelationsPlugin: Plugin = (builder) => { const foreignTable = constraint.foreignClassId ? introspectionResultsByKind.classById[constraint.foreignClassId] : null; + if (!foreignTable) { throw new Error( `Could not find the foreign table (constraint: ${constraint.name})` @@ -75,6 +76,7 @@ const PgConnectionArgFilterForwardRelationsPlugin: Plugin = (builder) => { const foreignKeyAttributes = constraint.foreignKeyAttributeNums.map( (num) => foreignAttributes.filter((attr) => attr.num === num)[0] ); + if (keyAttributes.some((attr) => omit(attr, 'read'))) { return memo; } @@ -88,6 +90,7 @@ const PgConnectionArgFilterForwardRelationsPlugin: Plugin = (builder) => { foreignKeyAttributes, constraint, }); + return memo; }, []); @@ -232,6 +235,7 @@ const PgConnectionArgFilterForwardRelationsPlugin: Plugin = (builder) => { foreignTable, foreignTableTypeName ); + if (!ForeignTableFilterType) continue; addField( @@ -246,9 +250,11 @@ const PgConnectionArgFilterForwardRelationsPlugin: Plugin = (builder) => { ); const keyIsNullable = !keyAttributes.every((attr) => attr.isNotNull); + if (keyIsNullable) { const existsFieldName = inflection.filterForwardRelationExistsFieldName(fieldName); + addField( existsFieldName, `A related \`${fieldName}\` exists.`, diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts index b9520bc5a..111b40468 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts @@ -1,5 +1,6 @@ import type { Plugin } from 'graphile-build'; import type { QueryBuilder, SQL } from 'graphile-build-pg'; + import { ConnectionFilterResolver } from './PgConnectionArgFilterPlugin'; const PgConnectionArgFilterLogicalOperatorsPlugin: Plugin = (builder) => { @@ -34,6 +35,7 @@ const PgConnectionArgFilterLogicalOperatorsPlugin: Plugin = (builder) => { connectionFilterResolve(o, sourceAlias, Self.name, queryBuilder) ) .filter((x) => x != null); + return sqlFragments.length === 0 ? null : sql.query`(${sql.join(sqlFragments, ') and (')})`; @@ -44,6 +46,7 @@ const PgConnectionArgFilterLogicalOperatorsPlugin: Plugin = (builder) => { connectionFilterResolve(o, sourceAlias, Self.name, queryBuilder) ) .filter((x) => x != null); + return sqlFragments.length === 0 ? null : sql.query`(${sql.join(sqlFragments, ') or (')})`; @@ -55,6 +58,7 @@ const PgConnectionArgFilterLogicalOperatorsPlugin: Plugin = (builder) => { Self.name, queryBuilder ); + return sqlFragment == null ? null : sql.query`not (${sqlFragment})`; }, }; diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterOperatorsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterOperatorsPlugin.ts index 229c70e49..6de319d58 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterOperatorsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterOperatorsPlugin.ts @@ -12,6 +12,7 @@ import type { ConnectionFilterConfig } from './types'; const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { const { connectionFilterAllowedOperators, connectionFilterOperatorNames } = rawOptions as ConnectionFilterConfig; + builder.hook('build', (build) => { const { graphql: { @@ -290,16 +291,19 @@ const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { const pgType = (introspectionResultsByKind.type as PgType[]).find( (t) => t.name === pgTypeName ); + if (!pgType) { return null; } const gqlTypeName = pgGetGqlTypeByTypeIdAndModifier(pgType.id, null).name; + if (gqlTypeName === 'String') { // PostGraphile v4 handles all unknown types as Strings, so we can't trust // that the String operators are appropriate. Just return null so that the // fallback type name defined below is used. return null; } + return gqlTypeName; }; @@ -465,6 +469,7 @@ const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { resolveSqlValue: (input, pgType, pgTypeModifier) => { const rangeSubType = introspectionResultsByKind.typeById[(pgType as any).rangeSubTypeId]; + return sql.query`${gql2pg( input, (pgType as any).rangeSubTypeId, @@ -615,6 +620,7 @@ const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { | 'Scalar'; }; }; + if ( !isPgConnectionFilterOperators || !pgConnectionFilterOperatorsCategory || @@ -637,6 +643,7 @@ const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { }; const operatorSpecs = operatorSpecsByCategory[pgConnectionFilterOperatorsCategory]; + if (!operatorSpecs) { return fields; } @@ -674,6 +681,7 @@ const PgConnectionArgFilterOperatorsPlugin: Plugin = (builder, rawOptions) => { isPgConnectionFilterOperator: true, } ); + return memo; }, {} diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterPlugin.ts index 8d80d588c..12e48a173 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterPlugin.ts @@ -23,6 +23,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { connectionFilterAllowNullInput, connectionFilterAllowEmptyObjectInput, } = rawOptions as ConnectionFilterConfig; + // Add `filter` input argument to connection and simple collection types builder.hook( 'GraphQLObjectType:fields:field:args', @@ -54,6 +55,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { }; const shouldAddFilter = isPgFieldConnection || isPgFieldSimpleCollection; + if (!shouldAddFilter) return args; if (!source) return args; @@ -73,6 +75,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { : (build.pgIntrospectionResultsByKind.type as PgType[]).find( (t) => t.id === returnTypeId ); + if (!returnType) { return args; } @@ -82,6 +85,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { : pgGetGqlTypeByTypeIdAndModifier(returnTypeId, null).name; const filterTypeName = inflection.filterType(nodeTypeName); const nodeType = getTypeByName(nodeTypeName); + if (!nodeType) { return args; } @@ -96,6 +100,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { nodeSource, nodeTypeName ); + if (!FilterType) { return args; } @@ -113,6 +118,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { returnType, null ); + if (sqlFragment != null) { queryBuilder.where(sqlFragment); } @@ -158,6 +164,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { 'Null literals are forbidden in filter argument input.' ); } + return null; }; @@ -167,6 +174,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { 'Empty objects are forbidden in filter argument input.' ); } + return null; }; @@ -182,6 +190,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { resolve: ConnectionFilterResolver ) => { const existingResolvers = connectionFilterResolvers[typeName] || {}; + if (existingResolvers[fieldName]) { return; } @@ -209,6 +218,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { if (isEmptyObject(value)) return handleEmptyObjectInput(); const resolversByFieldName = connectionFilterResolvers[typeName]; + if (resolversByFieldName && resolversByFieldName[key]) { return resolversByFieldName[key]({ sourceAlias, @@ -239,6 +249,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { const pgType = introspectionResultsByKind.typeById[pgTypeId]; const allowedPgTypeTypes = ['b', 'd', 'e', 'r']; + if (!allowedPgTypeTypes.includes(pgType.type)) { // Not a base, domain, enum, or range type? Skip. return null; @@ -260,6 +271,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { const pgGetSimpleType = (pgType: PgType) => pgGetNonDomainType(pgGetNonRangeType(pgGetNonArrayType(pgType))); const pgSimpleType = pgGetSimpleType(pgType); + if (!pgSimpleType) return null; if ( !( @@ -280,9 +292,11 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { // Establish field type and field input type const fieldType: GraphQLType | undefined = pgGetGqlTypeByTypeIdAndModifier(pgTypeId, pgTypeModifier); + if (!fieldType) return null; const fieldInputType: GraphQLType | undefined = pgGetGqlInputTypeByTypeIdAndModifier(pgTypeId, pgTypeModifier); + if (!fieldInputType) return null; // Avoid exposing filter operators on unrecognized types that PostGraphile handles as Strings @@ -299,6 +313,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { const citextPgType = (introspectionResultsByKind.type as PgType[]).find( (t) => t.name === 'citext' ); + if (citextPgType) { actualStringPgTypeIds.push(citextPgType.id); } @@ -358,9 +373,11 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { : inflection.filterFieldType(namedType.name); const existingType = connectionFilterTypesByTypeName[operatorsTypeName]; + if (existingType) { return existingType; } + return newWithHooks( GraphQLInputObjectType, { @@ -388,6 +405,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { nodeTypeName: string ) => { const existingType = connectionFilterTypesByTypeName[filterTypeName]; + if (existingType) { return existingType; } @@ -395,6 +413,7 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { name: filterTypeName, fields: {}, }); + connectionFilterTypesByTypeName[filterTypeName] = placeholder; const FilterType = newWithHooks( GraphQLInputObjectType, @@ -408,7 +427,9 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { }, true ); + connectionFilterTypesByTypeName[filterTypeName] = FilterType; + return FilterType; }; @@ -430,30 +451,36 @@ const PgConnectionArgFilterPlugin: Plugin = (builder, rawOptions) => { ) => { if (!typeNames) { const msg = `Missing first argument 'typeNames' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`; + throw new Error(msg); } if (!operatorName) { const msg = `Missing second argument 'operatorName' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`; + throw new Error(msg); } if (!resolveType) { const msg = `Missing fourth argument 'resolveType' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`; + throw new Error(msg); } if (!resolve) { const msg = `Missing fifth argument 'resolve' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`; + throw new Error(msg); } const { connectionFilterScalarOperators } = build; const gqlTypeNames = Array.isArray(typeNames) ? typeNames : [typeNames]; + for (const gqlTypeName of gqlTypeNames) { if (!connectionFilterScalarOperators[gqlTypeName]) { connectionFilterScalarOperators[gqlTypeName] = {}; } if (connectionFilterScalarOperators[gqlTypeName][operatorName]) { const msg = `Operator '${operatorName}' already exists for type '${gqlTypeName}'.`; + throw new Error(msg); } connectionFilterScalarOperators[gqlTypeName][operatorName] = { diff --git a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterRecordFunctionsPlugin.ts b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterRecordFunctionsPlugin.ts index 12cd41ed4..22957e774 100644 --- a/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterRecordFunctionsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/src/PgConnectionArgFilterRecordFunctionsPlugin.ts @@ -11,6 +11,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( ) => { const { connectionFilterSetofFunctions } = rawOptions as ConnectionFilterConfig; + builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => { const { extend, @@ -42,6 +43,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( // Must return a `RECORD` type const isRecordLike = proc.returnTypeId === '2249'; + if (!isRecordLike) return fields; // Must be marked @filterable OR enabled via plugin option @@ -57,6 +59,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(proc.argNames[idx] || ''); } + return prev; }, []); const outputArgTypes = proc.argTypeIds.reduce( @@ -64,6 +67,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, [] @@ -80,6 +84,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( outputArgName, idx + 1 ); + if (memo[fieldName]) { throw new Error( `Tried to register field name '${fieldName}' twice in '${describePgEntity( @@ -91,6 +96,7 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( name: outputArgName, type: outputArgTypes[idx], }; + return memo; }, {} @@ -103,9 +109,11 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( outputArg.type.id, null ); + if (!OperatorsType) { return memo; } + return extend(memo, { [fieldName]: fieldWithHooks( fieldName, @@ -160,4 +168,5 @@ const PgConnectionArgFilterRecordFunctionsPlugin: Plugin = ( return extend(fields, outputArgFields); }); }; + export default PgConnectionArgFilterRecordFunctionsPlugin; diff --git a/graphile/graphile-plugin-connection-filter/src/index.ts b/graphile/graphile-plugin-connection-filter/src/index.ts index 329d74c98..9629e0285 100644 --- a/graphile/graphile-plugin-connection-filter/src/index.ts +++ b/graphile/graphile-plugin-connection-filter/src/index.ts @@ -4,8 +4,8 @@ import type { Plugin } from 'graphile-build'; import ConnectionArgFilterPlugin from './ConnectionArgFilterPlugin'; import PgConnectionArgFilterBackwardRelationsPlugin from './PgConnectionArgFilterBackwardRelationsPlugin'; import PgConnectionArgFilterColumnsPlugin from './PgConnectionArgFilterColumnsPlugin'; -import PgConnectionArgFilterComputedColumnsPlugin from './PgConnectionArgFilterComputedColumnsPlugin'; import PgConnectionArgFilterCompositeTypeColumnsPlugin from './PgConnectionArgFilterCompositeTypeColumnsPlugin'; +import PgConnectionArgFilterComputedColumnsPlugin from './PgConnectionArgFilterComputedColumnsPlugin'; import PgConnectionArgFilterForwardRelationsPlugin from './PgConnectionArgFilterForwardRelationsPlugin'; import PgConnectionArgFilterLogicalOperatorsPlugin from './PgConnectionArgFilterLogicalOperatorsPlugin'; import PgConnectionArgFilterOperatorsPlugin from './PgConnectionArgFilterOperatorsPlugin'; @@ -46,6 +46,7 @@ const PostGraphileConnectionFilterPlugin: Plugin = ( ); } }; + depends('graphile-build-pg', '^4.5.0'); build.versions = build.extend(build.versions, { [pkg.name]: pkg.version }); diff --git a/graphile/graphile-plugin-connection-filter/test-utils/customOperatorsPlugin.ts b/graphile/graphile-plugin-connection-filter/test-utils/customOperatorsPlugin.ts index f46796852..093bfa28f 100644 --- a/graphile/graphile-plugin-connection-filter/test-utils/customOperatorsPlugin.ts +++ b/graphile/graphile-plugin-connection-filter/test-utils/customOperatorsPlugin.ts @@ -1,4 +1,4 @@ -import type { SchemaBuilder, Plugin } from 'graphile-build'; +import type { Plugin,SchemaBuilder } from 'graphile-build'; import type { SQL } from 'graphile-build-pg'; import type { GraphQLScalarType } from 'graphql'; diff --git a/graphile/graphile-plugin-connection-filter/test-utils/printSchema.ts b/graphile/graphile-plugin-connection-filter/test-utils/printSchema.ts index 8a281902a..daba43cfe 100644 --- a/graphile/graphile-plugin-connection-filter/test-utils/printSchema.ts +++ b/graphile/graphile-plugin-connection-filter/test-utils/printSchema.ts @@ -3,5 +3,6 @@ import { lexicographicSortSchema, printSchema } from 'graphql/utilities'; export const printSchemaOrdered = (originalSchema: GraphQLSchema): string => { const schema = buildASTSchema(parse(printSchema(originalSchema))); + return printSchema(lexicographicSortSchema(schema)); }; diff --git a/graphile/graphile-plugin-fulltext-filter/__tests__/fulltext.test.ts b/graphile/graphile-plugin-fulltext-filter/__tests__/fulltext.test.ts index eed573d89..8c4288001 100644 --- a/graphile/graphile-plugin-fulltext-filter/__tests__/fulltext.test.ts +++ b/graphile/graphile-plugin-fulltext-filter/__tests__/fulltext.test.ts @@ -1,9 +1,9 @@ +import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; import type { GraphQLQueryFnObj, GraphQLTestContext } from 'graphile-test'; import { getConnectionsObject, seed } from 'graphile-test'; import { buildClientSchema, getIntrospectionQuery } from 'graphql'; import { join } from 'path'; import type { PgTestClient } from 'pgsql-test/test-client'; -import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; import PostGraphileFulltextFilterPlugin from '../src'; @@ -154,6 +154,7 @@ it('table with unfiltered full-text field works', async () => { expect(result.errors).toBeUndefined(); const data = result.data?.allJobs.nodes; + expect(data).toHaveLength(2); data?.forEach((n: any) => expect(n.fullTextRank).not.toBeNull()); @@ -179,6 +180,7 @@ it('table with unfiltered full-text field works', async () => { expect(bananaResult.errors).toBeUndefined(); const bananaData = bananaResult.data?.allJobs.nodes; + expect(bananaData).toHaveLength(1); bananaData?.forEach((n: any) => expect(n.fullTextRank).not.toBeNull()); }); @@ -209,6 +211,7 @@ it('table with unfiltered full-text field works', async () => { expect(result.errors).toBeUndefined(); const data = result.data?.allJobs.nodes; + expect(data).toHaveLength(2); data?.forEach((n: any) => expect(n.fullTextRank).toBeNull()); }); @@ -253,6 +256,7 @@ it('table with unfiltered full-text field works', async () => { expect(result.errors).toBeUndefined(); const data = result.data?.allJobs.nodes; + expect(data).toHaveLength(2); data?.forEach((n: any) => { expect(n.fullTextRank).not.toBeNull(); @@ -282,6 +286,7 @@ it('table with unfiltered full-text field works', async () => { expect(potatoResult.errors).toBeUndefined(); const potatoData = potatoResult.data?.allJobs.nodes; + expect(potatoData).toHaveLength(1); potatoData?.forEach((n: any) => { expect(n.fullTextRank).toBeNull(); diff --git a/graphile/graphile-plugin-fulltext-filter/src/index.ts b/graphile/graphile-plugin-fulltext-filter/src/index.ts index 7ca981811..dc6ddcac7 100644 --- a/graphile/graphile-plugin-fulltext-filter/src/index.ts +++ b/graphile/graphile-plugin-fulltext-filter/src/index.ts @@ -1,6 +1,6 @@ -import { Tsquery } from 'pg-tsquery'; -import { omit } from 'graphile-build-pg'; import type { Plugin } from 'graphile-build'; +import { omit } from 'graphile-build-pg'; +import { Tsquery } from 'pg-tsquery'; const tsquery = new Tsquery(); @@ -26,7 +26,8 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { const columnName = attr.kind === 'procedure' ? attr.name.substring(table.name.length + 1) - : this._columnName(attr, { skipRowId: true }); // eslint-disable-line no-underscore-dangle + : this._columnName(attr, { skipRowId: true }); + return this.constantCase( `${columnName}_rank_${ascending ? 'asc' : 'desc'}` ); @@ -46,6 +47,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { const tsvectorType = introspectionResultsByKind.type.find( (t: any) => t.name === 'tsvector' ); + if (!tsvectorType) { throw new Error('Unable to find tsvector type through introspection.'); } @@ -93,6 +95,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { } const InputType = getGqlInputTypeByTypeIdAndModifier(pgTsvType.id, null); + addConnectionFilterOperator( InputType.name, 'matches', @@ -106,8 +109,10 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { queryBuilder: QueryBuilder ) => { const tsQueryString = `${tsquery.parse(input) || ''}`; + queryBuilder.__fts_ranks = queryBuilder.__fts_ranks || {}; queryBuilder.__fts_ranks[fieldName] = [identifier, tsQueryString]; + return sql.query`${identifier} @@ to_tsquery(${sql.value(tsQueryString)})`; }, { @@ -149,6 +154,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { type.namespaceId === table.namespaceId && type.classId === table.id ); + if (!tableType) { throw new Error('Could not determine the type of this table.'); } @@ -178,6 +184,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { addDataGenerator(({ alias }: any) => ({ pgQuery: (queryBuilder: QueryBuilder) => { const { parentQueryBuilder } = queryBuilder; + if ( !parentQueryBuilder || !parentQueryBuilder.__fts_ranks || @@ -187,12 +194,14 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { } const [identifier, tsQueryString] = parentQueryBuilder.__fts_ranks[baseFieldName]; + queryBuilder.select?.( sql.fragment`ts_rank(${identifier}, to_tsquery(${sql.value(tsQueryString)}))`, alias ); }, })); + return { description: `Full-text search ranking when filtered by \`${baseFieldName}\`.`, type: GraphQLFloat, @@ -207,6 +216,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { const tsvFields = tsvColumns.reduce((memo: any, attr: any) => { const fieldName = inflection.column(attr); const rankFieldName = inflection.pgTsvRank(fieldName); + memo[rankFieldName] = newRankField(fieldName, rankFieldName); return memo; @@ -216,6 +226,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { const psuedoColumnName = proc.name.substring(table.name.length + 1); const fieldName = inflection.computedColumn(psuedoColumnName, proc, table); const rankFieldName = inflection.pgTsvRank(fieldName); + memo[rankFieldName] = newRankField(fieldName, rankFieldName); return memo; @@ -248,6 +259,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { type.namespaceId === table.namespaceId && type.classId === table.id ); + if (!tableType) { throw new Error('Could not determine the type of this table.'); } @@ -301,6 +313,7 @@ const PostGraphileFulltextFilterPlugin: Plugin = (builder) => { } const [identifier, tsQueryString] = queryBuilder.__fts_ranks[fieldName]; + return sql.fragment`ts_rank(${identifier}, to_tsquery(${sql.value(tsQueryString)}))`; }; diff --git a/graphile/graphile-postgis/__tests__/integration/queries.test.ts b/graphile/graphile-postgis/__tests__/integration/queries.test.ts index eb95f1f04..458054d03 100644 --- a/graphile/graphile-postgis/__tests__/integration/queries.test.ts +++ b/graphile/graphile-postgis/__tests__/integration/queries.test.ts @@ -1,7 +1,8 @@ import '../../test-utils/env'; + import { readdirSync, readFileSync } from 'fs'; +import { getConnections, type GraphQLQueryFn,seed, snapshot } from 'graphile-test'; import { join } from 'path'; -import { getConnections, seed, snapshot, type GraphQLQueryFn } from 'graphile-test'; import type { PgTestClient } from 'pgsql-test/test-client'; import PostgisPlugin from '../../src'; @@ -44,6 +45,7 @@ describe('integration queries', () => { it.each(queryFileNames)('%s', async (queryFileName) => { const queryText = readFileSync(join(queriesDir, queryFileName), 'utf8'); const result = await query(queryText); + expect(snapshot(result)).toMatchSnapshot(); }); }); diff --git a/graphile/graphile-postgis/__tests__/schema.test.ts b/graphile/graphile-postgis/__tests__/schema.test.ts index 5896d3220..b830cc614 100644 --- a/graphile/graphile-postgis/__tests__/schema.test.ts +++ b/graphile/graphile-postgis/__tests__/schema.test.ts @@ -1,4 +1,5 @@ import '../test-utils/env'; + import type { GraphQLQueryFn } from 'graphile-test'; import type { PgTestClient } from 'pgsql-test/test-client'; @@ -19,6 +20,7 @@ describe.each(SCHEMAS)('%s schema snapshot', (schemaName) => { beforeAll(async () => { const connections = await createConnectionsForSchema(schemaName); + ({ query, teardown, db } = connections); }); @@ -33,6 +35,7 @@ describe.each(SCHEMAS)('%s schema snapshot', (schemaName) => { it('prints a schema with this plugin', async () => { const printedSchema = await getSchemaSnapshot(query); + expect(printedSchema).toMatchSnapshot(); }); }); diff --git a/graphile/graphile-postgis/__tests__/version.test.ts b/graphile/graphile-postgis/__tests__/version.test.ts index 4ab2f3b76..2086522fb 100644 --- a/graphile/graphile-postgis/__tests__/version.test.ts +++ b/graphile/graphile-postgis/__tests__/version.test.ts @@ -15,12 +15,14 @@ const getBuildHook = (): HookFn => { if (!hook) { throw new Error('Version plugin hook was not registered'); } + return hook; }; describe('PostgisVersionPlugin', () => { it('throws when graphile-build is too old to provide versions', () => { const hook = getBuildHook(); + expect(() => hook({ graphileBuildVersion: '0.0.0' })).toThrow( /graphile-build@\^4\.1\.0/i ); @@ -28,6 +30,7 @@ describe('PostgisVersionPlugin', () => { it('throws when graphile-build-pg requirement is unmet', () => { const hook = getBuildHook(); + expect(() => hook({ versions: { 'graphile-build': '4.1.0' }, @@ -47,6 +50,7 @@ describe('PostgisVersionPlugin', () => { graphileBuildVersion: '4.14.1' }; const result = hook(build); + expect(result.versions['graphile-postgis']).toBeDefined(); }); }); diff --git a/graphile/graphile-postgis/src/PostgisExtensionDetectionPlugin.ts b/graphile/graphile-postgis/src/PostgisExtensionDetectionPlugin.ts index 264809d20..e43b15a39 100644 --- a/graphile/graphile-postgis/src/PostgisExtensionDetectionPlugin.ts +++ b/graphile/graphile-postgis/src/PostgisExtensionDetectionPlugin.ts @@ -10,9 +10,11 @@ const PostgisExtensionDetectionPlugin: Plugin = (builder) => { const pgGISExtension = introspectionResultsByKind.extension.find( (extension: PgExtension) => extension.name === 'postgis' ); + // Check we have the postgis extension if (!pgGISExtension) { console.warn('PostGIS extension not found in database; skipping'); + return postgisBuild; } // Extract the geography and geometry types @@ -22,11 +24,13 @@ const PostgisExtensionDetectionPlugin: Plugin = (builder) => { const pgGISGeographyType = introspectionResultsByKind.type.find( (type: PgType) => type.name === 'geography' && type.namespaceId === pgGISExtension.namespaceId ); + if (!pgGISGeographyType || !pgGISGeometryType) { throw new Error( "PostGIS is installed, but we couldn't find the geometry/geography types!" ); } + return postgisBuild.extend(postgisBuild, { pgGISGraphQLTypesByTypeAndSubtype: {}, pgGISGraphQLInterfaceTypesByType: {}, diff --git a/graphile/graphile-postgis/src/PostgisRegisterTypesPlugin.ts b/graphile/graphile-postgis/src/PostgisRegisterTypesPlugin.ts index 5b01f55f7..592c643f9 100644 --- a/graphile/graphile-postgis/src/PostgisRegisterTypesPlugin.ts +++ b/graphile/graphile-postgis/src/PostgisRegisterTypesPlugin.ts @@ -3,16 +3,14 @@ import type { PgType } from 'graphile-build-pg'; import type { SQL } from 'graphile-build-pg/node8plus/QueryBuilder'; import type { GraphQLInterfaceType, - GraphQLOutputType, - GraphQLObjectType, GraphQLScalarType, GraphQLSchemaConfig } from 'graphql'; import { GisSubtype } from './constants'; import makeGeoJSONType from './makeGeoJSONType'; -import { getGISTypeDetails, getGISTypeModifier, getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisTypeDetails, PostgisBuild } from './types'; +import { getGISTypeDetails, getGISTypeModifier, getGISTypeName } from './utils'; const SUBTYPES: GisSubtype[] = [ GisSubtype.Point, @@ -30,6 +28,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { builder.hook('build', (build: Build): Build => { const rawBuild = build as PostgisBuild; const GeoJSON = makeGeoJSONType(rawBuild.graphql, rawBuild.inflection.builtin('GeoJSON')); + rawBuild.addType(GeoJSON); return rawBuild.extend(rawBuild, { @@ -42,6 +41,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { srid: number = 0 ) { const typeModifier = getGISTypeModifier(subtype, hasZ, hasM, srid); + return this.pgGetGqlTypeByTypeIdAndModifier(pgGISType.id, typeModifier); }, pgGISIncludedTypes: [] as GisGraphQLType[], @@ -74,12 +74,14 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { pgGISExtension: POSTGIS, pgGISIncludeType: includeType } = rawBuild; + if (!GEOMETRY_TYPE || !GEOGRAPHY_TYPE) { return input; } // console.warn('PostGIS plugin enabled'); const GeoJSON = getTypeByName(inflection.builtin('GeoJSON')) as GraphQLScalarType | undefined; + if (!GeoJSON) { throw new Error('GeoJSON type was not registered on the build'); } @@ -93,6 +95,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { const getGisInterface = (type: PgType): GraphQLInterfaceType => { const zmflag = -1; // no dimensional constraint; could be xy/xyz/xym/xyzm + ensureInterfaceStore(type); if (!interfacesMap[type.id][zmflag]) { interfacesMap[type.id][zmflag] = newWithHooks( @@ -111,6 +114,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { }, resolveType(value: GisFieldValue) { const Type = constructedTypes[type.id]?.[value.__gisType]; + return Type instanceof GraphQLObjectType ? Type : undefined; }, description: `All ${type.name} types implement this interface` @@ -126,16 +130,19 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { for (const hasM of [false, true]) { const typeModifier = getGISTypeModifier(subtype, hasZ, hasM, 0); const Type = getGisType(type, typeModifier); + includeType(Type); } } } } + return interfacesMap[type.id][zmflag]; }; const getGisDimensionInterface = (type: PgType, hasZ: boolean, hasM: boolean): GraphQLInterfaceType => { const zmflag = (hasZ ? 2 : 0) + (hasM ? 1 : 0); // Equivalent to ST_Zmflag: https://postgis.net/docs/ST_Zmflag.html const coords: Record = { 0: 'XY', 1: 'XYM', 2: 'XYZ', 3: 'XYZM' }; + ensureInterfaceStore(type); if (!interfacesMap[type.id][zmflag]) { interfacesMap[type.id][zmflag] = newWithHooks( @@ -154,6 +161,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { }, resolveType(value: GisFieldValue) { const Type = constructedTypes[type.id]?.[value.__gisType]; + return Type instanceof GraphQLObjectType ? Type : undefined; }, description: `All ${type.name} ${coords[zmflag]} types implement this interface` @@ -167,15 +175,18 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { for (const subtype of SUBTYPES) { const typeModifier = getGISTypeModifier(subtype, hasZ, hasM, 0); const Type = getGisType(type, typeModifier); + includeType(Type); } } + return interfacesMap[type.id][zmflag]; }; const getGisType = (type: PgType, typeModifier: number): GisGraphQLType => { const typeId = type.id; const typeDetails: GisTypeDetails = getGISTypeDetails(typeModifier); const { subtype, hasZ, hasM, srid } = typeDetails; + // console.warn( // `Getting ${type.name} type ${type.id}|${typeModifier}|${subtype}|${hasZ}|${hasM}|${srid}` // ); @@ -183,6 +194,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { constructedTypes[typeId] = {}; } const typeModifierKey = typeModifier ?? -1; + if (!pgTweaksByTypeIdAndModifer[typeId]) { pgTweaksByTypeIdAndModifer[typeId] = {}; } @@ -214,6 +226,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { 'st_asgeojson' // MUST be lowercase! )}(${fragment})::JSON` ]; + return sql.fragment`(case when ${fragment} is null then null else json_build_object( ${sql.join(params, ', ')} ) end)`; @@ -221,6 +234,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { } const gisTypeKey: string | number = typeModifier != null ? getGISTypeName(subtype, hasZ, hasM) : -1; + if (!constructedTypes[typeId][gisTypeKey]) { if (typeModifierKey === -1) { constructedTypes[typeId][gisTypeKey] = getGisInterface(type); @@ -239,6 +253,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { (introspectionType: PgType) => introspectionType.name === 'json' && introspectionType.namespaceName === 'pg_catalog' ); + if (!intType || !jsonType) { throw new Error('Unable to locate built-in int4/json types'); } @@ -274,6 +289,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { ); } } + return constructedTypes[typeId][gisTypeKey]; }; @@ -309,6 +325,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { pgRegisterGqlTypeByTypeId(GEOMETRY_TYPE.id, (_set: Record, typeModifier: number) => { return getGisType(GEOMETRY_TYPE, typeModifier); }); + return input; }, ['PostgisTypes'], @@ -319,6 +336,7 @@ const PostgisRegisterTypesPlugin: Plugin = (builder) => { builder.hook('GraphQLSchema', (schemaConfig, build: Build) => { const postgisBuild = build as PostgisBuild; const existingTypes = schemaConfig.types ?? []; + return { ...schemaConfig, types: [...existingTypes, ...postgisBuild.pgGISIncludedTypes] diff --git a/graphile/graphile-postgis/src/PostgisVersionPlugin.ts b/graphile/graphile-postgis/src/PostgisVersionPlugin.ts index 5a586dd32..ae49abef9 100644 --- a/graphile/graphile-postgis/src/PostgisVersionPlugin.ts +++ b/graphile/graphile-postgis/src/PostgisVersionPlugin.ts @@ -23,6 +23,7 @@ const plugin: Plugin = (builder) => { ); } }; + depends('graphile-build-pg', '^4.4.0'); // Register this plugin diff --git a/graphile/graphile-postgis/src/Postgis_GeometryCollection_GeometriesPlugin.ts b/graphile/graphile-postgis/src/Postgis_GeometryCollection_GeometriesPlugin.ts index 034c2b428..ee2d4a7b5 100644 --- a/graphile/graphile-postgis/src/Postgis_GeometryCollection_GeometriesPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_GeometryCollection_GeometriesPlugin.ts @@ -1,10 +1,10 @@ +import type { GeometryCollection } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { GeometryCollection } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisGeometryCollectionGeometriesPlugin: Plugin = (builder) => { builder.hook( @@ -17,6 +17,7 @@ const PostgisGeometryCollectionGeometriesPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if ( !isPgGISType || !pgGISType || @@ -33,8 +34,10 @@ const PostgisGeometryCollectionGeometriesPlugin: Plugin = (builder) => { const { hasZ, hasM } = pgGISTypeDetails; const zmflag = (hasZ ? 2 : 0) + (hasM ? 1 : 0); // Equivalent to ST_Zmflag: https://postgis.net/docs/ST_Zmflag.html const Interface = pgGISGraphQLInterfaceTypesByType[pgGISType.id][zmflag]; + if (!Interface) { console.warn("Unexpectedly couldn't find the interface"); + return fields; } @@ -43,11 +46,14 @@ const PostgisGeometryCollectionGeometriesPlugin: Plugin = (builder) => { type: new GraphQLList(Interface), resolve(data: GisFieldValue) { const geometryCollection = data.__geojson as GeometryCollection; + return geometryCollection.geometries.map((geom) => { const subtype = GisSubtype[geom.type as keyof typeof GisSubtype]; + if (subtype === undefined) { throw new Error(`Unsupported geometry subtype ${geom.type}`); } + return { __gisType: getGISTypeName(subtype, hasZ, hasM), __srid: data.__srid, @@ -60,4 +66,5 @@ const PostgisGeometryCollectionGeometriesPlugin: Plugin = (builder) => { } ); }; + export default PostgisGeometryCollectionGeometriesPlugin; diff --git a/graphile/graphile-postgis/src/Postgis_LineString_PointsPlugin.ts b/graphile/graphile-postgis/src/Postgis_LineString_PointsPlugin.ts index 74817e1dc..f5455d63c 100644 --- a/graphile/graphile-postgis/src/Postgis_LineString_PointsPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_LineString_PointsPlugin.ts @@ -1,10 +1,10 @@ +import type { LineString, Point as GeoPoint } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { LineString, Point as GeoPoint } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisLineStringPointsPlugin: Plugin = (builder) => { builder.hook( @@ -13,6 +13,7 @@ const PostgisLineStringPointsPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if ( !isPgGISType || !pgGISType || @@ -44,6 +45,7 @@ const PostgisLineStringPointsPlugin: Plugin = (builder) => { type: new GraphQLList(Point), resolve(data: GisFieldValue) { const lineString = data.__geojson as LineString; + return lineString.coordinates.map((coord) => { return { __gisType: getGISTypeName(GisSubtype.Point, hasZ, hasM), @@ -60,4 +62,5 @@ const PostgisLineStringPointsPlugin: Plugin = (builder) => { } ); }; + export default PostgisLineStringPointsPlugin; diff --git a/graphile/graphile-postgis/src/Postgis_MultiLineString_LineStringsPlugin.ts b/graphile/graphile-postgis/src/Postgis_MultiLineString_LineStringsPlugin.ts index e68fb5817..a82bed1f4 100644 --- a/graphile/graphile-postgis/src/Postgis_MultiLineString_LineStringsPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_MultiLineString_LineStringsPlugin.ts @@ -1,10 +1,10 @@ +import type { LineString, MultiLineString } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { LineString, MultiLineString } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisMultiLineStringLineStringsPlugin: Plugin = (builder) => { builder.hook( @@ -13,6 +13,7 @@ const PostgisMultiLineStringLineStringsPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if ( !isPgGISType || !pgGISType || @@ -44,6 +45,7 @@ const PostgisMultiLineStringLineStringsPlugin: Plugin = (builder) => { type: new GraphQLList(LineString), resolve(data: GisFieldValue) { const multiLineString = data.__geojson as MultiLineString; + return multiLineString.coordinates.map((coord) => ({ __gisType: getGISTypeName(GisSubtype.LineString, hasZ, hasM), __srid: data.__srid, @@ -58,4 +60,5 @@ const PostgisMultiLineStringLineStringsPlugin: Plugin = (builder) => { } ); }; + export default PostgisMultiLineStringLineStringsPlugin; diff --git a/graphile/graphile-postgis/src/Postgis_MultiPoint_PointsPlugin.ts b/graphile/graphile-postgis/src/Postgis_MultiPoint_PointsPlugin.ts index b8539f95b..9962309bf 100644 --- a/graphile/graphile-postgis/src/Postgis_MultiPoint_PointsPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_MultiPoint_PointsPlugin.ts @@ -1,10 +1,10 @@ +import type { MultiPoint, Point as GeoPoint } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { MultiPoint, Point as GeoPoint } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisMultiPointPointsPlugin: Plugin = (builder) => { builder.hook( @@ -13,6 +13,7 @@ const PostgisMultiPointPointsPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if ( !isPgGISType || !pgGISType || @@ -44,6 +45,7 @@ const PostgisMultiPointPointsPlugin: Plugin = (builder) => { type: new GraphQLList(Point), resolve(data: GisFieldValue) { const multiPoint = data.__geojson as MultiPoint; + return multiPoint.coordinates.map((coord) => ({ __gisType: getGISTypeName(GisSubtype.Point, hasZ, hasM), __srid: data.__srid, @@ -58,4 +60,5 @@ const PostgisMultiPointPointsPlugin: Plugin = (builder) => { } ); }; + export default PostgisMultiPointPointsPlugin; diff --git a/graphile/graphile-postgis/src/Postgis_MultiPolygon_PolygonsPlugin.ts b/graphile/graphile-postgis/src/Postgis_MultiPolygon_PolygonsPlugin.ts index bbeea0f44..aa819e189 100644 --- a/graphile/graphile-postgis/src/Postgis_MultiPolygon_PolygonsPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_MultiPolygon_PolygonsPlugin.ts @@ -1,10 +1,10 @@ +import type { MultiPolygon, Polygon } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { MultiPolygon, Polygon } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisMultiPolygonPolygonsPlugin: Plugin = (builder) => { builder.hook( @@ -13,6 +13,7 @@ const PostgisMultiPolygonPolygonsPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if ( !isPgGISType || !pgGISType || @@ -44,6 +45,7 @@ const PostgisMultiPolygonPolygonsPlugin: Plugin = (builder) => { type: new GraphQLList(PolygonType), resolve(data: GisFieldValue) { const multiPolygon = data.__geojson as MultiPolygon; + return multiPolygon.coordinates.map((coord) => ({ __gisType: getGISTypeName(GisSubtype.Polygon, hasZ, hasM), __srid: data.__srid, @@ -58,4 +60,5 @@ const PostgisMultiPolygonPolygonsPlugin: Plugin = (builder) => { } ); }; + export default PostgisMultiPolygonPolygonsPlugin; diff --git a/graphile/graphile-postgis/src/Postgis_Point_LatitudeLongitudePlugin.ts b/graphile/graphile-postgis/src/Postgis_Point_LatitudeLongitudePlugin.ts index 3830d6021..2ec5d5be7 100644 --- a/graphile/graphile-postgis/src/Postgis_Point_LatitudeLongitudePlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_Point_LatitudeLongitudePlugin.ts @@ -1,6 +1,6 @@ +import type { Point } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { Point } from 'geojson'; import { GisSubtype } from './constants'; import type { GisFieldValue, GisScope, PostgisBuild } from './types'; @@ -12,6 +12,7 @@ const PostgisPointLatitudeLongitudePlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if (!isPgGISType || !pgGISType || !pgGISTypeDetails || pgGISTypeDetails.subtype !== GisSubtype.Point) { return fields; } @@ -23,11 +24,13 @@ const PostgisPointLatitudeLongitudePlugin: Plugin = (builder) => { const xFieldName = inflection.gisXFieldName(pgGISType); const yFieldName = inflection.gisYFieldName(pgGISType); const zFieldName = inflection.gisZFieldName(pgGISType); + return extend(fields, { [xFieldName]: { type: new GraphQLNonNull(GraphQLFloat), resolve(data: GisFieldValue) { const point = data.__geojson as Point; + return point.coordinates[0]; } }, @@ -35,6 +38,7 @@ const PostgisPointLatitudeLongitudePlugin: Plugin = (builder) => { type: new GraphQLNonNull(GraphQLFloat), resolve(data: GisFieldValue) { const point = data.__geojson as Point; + return point.coordinates[1]; } }, @@ -44,6 +48,7 @@ const PostgisPointLatitudeLongitudePlugin: Plugin = (builder) => { type: new GraphQLNonNull(GraphQLFloat), resolve(data: GisFieldValue) { const point = data.__geojson as Point; + return point.coordinates[2]; } } @@ -53,4 +58,5 @@ const PostgisPointLatitudeLongitudePlugin: Plugin = (builder) => { } ); }; + export default PostgisPointLatitudeLongitudePlugin; diff --git a/graphile/graphile-postgis/src/Postgis_Polygon_RingsPlugin.ts b/graphile/graphile-postgis/src/Postgis_Polygon_RingsPlugin.ts index 3ddc2c14e..4ade92319 100644 --- a/graphile/graphile-postgis/src/Postgis_Polygon_RingsPlugin.ts +++ b/graphile/graphile-postgis/src/Postgis_Polygon_RingsPlugin.ts @@ -1,10 +1,10 @@ +import type { LineString, Polygon } from 'geojson'; import type { Build, Plugin } from 'graphile-build'; import type { GraphQLFieldConfigMap } from 'graphql'; -import type { LineString, Polygon } from 'geojson'; import { GisSubtype } from './constants'; -import { getGISTypeName } from './utils'; import type { GisFieldValue, GisGraphQLType, GisScope, PostgisBuild } from './types'; +import { getGISTypeName } from './utils'; const PostgisPolygonRingsPlugin: Plugin = (builder) => { builder.hook( @@ -13,6 +13,7 @@ const PostgisPolygonRingsPlugin: Plugin = (builder) => { const { scope: { isPgGISType, pgGISType, pgGISTypeDetails } } = context as typeof context & { scope: GisScope }; + if (!isPgGISType || !pgGISType || !pgGISTypeDetails || pgGISTypeDetails.subtype !== GisSubtype.Polygon) { return fields; } @@ -39,6 +40,7 @@ const PostgisPolygonRingsPlugin: Plugin = (builder) => { type: LineStringType, resolve(data: GisFieldValue) { const polygon = data.__geojson as Polygon; + return { __gisType: getGISTypeName(GisSubtype.LineString, hasZ, hasM), __srid: data.__srid, @@ -53,6 +55,7 @@ const PostgisPolygonRingsPlugin: Plugin = (builder) => { type: new GraphQLList(LineStringType), resolve(data: GisFieldValue) { const polygon = data.__geojson as Polygon; + return polygon.coordinates.slice(1).map((coord) => ({ __gisType: getGISTypeName(GisSubtype.LineString, hasZ, hasM), __srid: data.__srid, @@ -67,4 +70,5 @@ const PostgisPolygonRingsPlugin: Plugin = (builder) => { } ); }; + export default PostgisPolygonRingsPlugin; diff --git a/graphile/graphile-postgis/src/index.ts b/graphile/graphile-postgis/src/index.ts index bd472a833..9b187d6d4 100644 --- a/graphile/graphile-postgis/src/index.ts +++ b/graphile/graphile-postgis/src/index.ts @@ -1,9 +1,5 @@ import type { Plugin } from 'graphile-build'; -import PostgisExtensionDetectionPlugin from './PostgisExtensionDetectionPlugin'; -import PostgisInflectionPlugin from './PostgisInflectionPlugin'; -import PostgisRegisterTypesPlugin from './PostgisRegisterTypesPlugin'; -import PostgisVersionPlugin from './PostgisVersionPlugin'; import Postgis_GeometryCollection_GeometriesPlugin from './Postgis_GeometryCollection_GeometriesPlugin'; import Postgis_LineString_PointsPlugin from './Postgis_LineString_PointsPlugin'; import Postgis_MultiLineString_LineStringsPlugin from './Postgis_MultiLineString_LineStringsPlugin'; @@ -11,6 +7,10 @@ import Postgis_MultiPoint_PointsPlugin from './Postgis_MultiPoint_PointsPlugin'; import Postgis_MultiPolygon_PolygonsPlugin from './Postgis_MultiPolygon_PolygonsPlugin'; import Postgis_Point_LatitudeLongitudePlugin from './Postgis_Point_LatitudeLongitudePlugin'; import Postgis_Polygon_RingsPlugin from './Postgis_Polygon_RingsPlugin'; +import PostgisExtensionDetectionPlugin from './PostgisExtensionDetectionPlugin'; +import PostgisInflectionPlugin from './PostgisInflectionPlugin'; +import PostgisRegisterTypesPlugin from './PostgisRegisterTypesPlugin'; +import PostgisVersionPlugin from './PostgisVersionPlugin'; const PostgisPlugin: Plugin = async (builder, options) => { await PostgisVersionPlugin(builder, options); @@ -41,17 +41,16 @@ const PostgisPlugin: Plugin = async (builder, options) => { }; export { - PostgisExtensionDetectionPlugin, - PostgisInflectionPlugin, - PostgisRegisterTypesPlugin, - PostgisVersionPlugin, Postgis_GeometryCollection_GeometriesPlugin, Postgis_LineString_PointsPlugin, Postgis_MultiLineString_LineStringsPlugin, Postgis_MultiPoint_PointsPlugin, Postgis_MultiPolygon_PolygonsPlugin, Postgis_Point_LatitudeLongitudePlugin, - Postgis_Polygon_RingsPlugin -}; + Postgis_Polygon_RingsPlugin, + PostgisExtensionDetectionPlugin, + PostgisInflectionPlugin, + PostgisRegisterTypesPlugin, + PostgisVersionPlugin}; export default PostgisPlugin; diff --git a/graphile/graphile-postgis/src/makeGeoJSONType.ts b/graphile/graphile-postgis/src/makeGeoJSONType.ts index 912aaa3e1..ccf4f55b9 100644 --- a/graphile/graphile-postgis/src/makeGeoJSONType.ts +++ b/graphile/graphile-postgis/src/makeGeoJSONType.ts @@ -44,9 +44,11 @@ export default function makeGeoJSONType( return parseFloat(ast.value); case Kind.OBJECT: { const value: Record = Object.create(null); + ast.fields.forEach((field) => { value[field.name.value] = parseLiteral(field.value, variables); }); + return value; } case Kind.LIST: @@ -55,6 +57,7 @@ export default function makeGeoJSONType( return null; case Kind.VARIABLE: { const variableName = ast.name.value; + return variables ? variables[variableName] : undefined; } default: diff --git a/graphile/graphile-postgis/src/types.ts b/graphile/graphile-postgis/src/types.ts index 15e2a7ea2..8c4cb5528 100644 --- a/graphile/graphile-postgis/src/types.ts +++ b/graphile/graphile-postgis/src/types.ts @@ -1,3 +1,4 @@ +import type { Geometry } from 'geojson'; import type { Build, Inflection } from 'graphile-build'; import type { PgExtension, PgIntrospectionResultsByKind, PgType } from 'graphile-build-pg'; import type { SQL } from 'graphile-build-pg/node8plus/QueryBuilder'; @@ -8,7 +9,6 @@ import type { GraphQLOutputType, GraphQLType } from 'graphql'; -import type { Geometry } from 'geojson'; import type { GisSubtype } from './constants'; diff --git a/graphile/graphile-postgis/src/utils.ts b/graphile/graphile-postgis/src/utils.ts index 50ab2be36..9b80f468e 100644 --- a/graphile/graphile-postgis/src/utils.ts +++ b/graphile/graphile-postgis/src/utils.ts @@ -3,6 +3,7 @@ import type { GisTypeDetails } from './types'; export const getGISTypeDetails = (modifier: number): GisTypeDetails => { const allZeroesHopefully = modifier >> 24; + if (allZeroesHopefully !== 0) { throw new Error('Unsupported PostGIS modifier'); } diff --git a/graphile/graphile-postgis/test-utils/helpers.ts b/graphile/graphile-postgis/test-utils/helpers.ts index 7688ece13..728802cc5 100644 --- a/graphile/graphile-postgis/test-utils/helpers.ts +++ b/graphile/graphile-postgis/test-utils/helpers.ts @@ -1,15 +1,14 @@ +import type { GraphQLQueryFn } from 'graphile-test'; +import { getConnections, seed } from 'graphile-test'; +import type { GraphQLSchema } from 'graphql'; import { buildASTSchema, buildClientSchema, getIntrospectionQuery, + type IntrospectionQuery, parse, - printSchema, - type IntrospectionQuery -} from 'graphql'; + printSchema} from 'graphql'; import { join } from 'path'; -import type { GraphQLSchema } from 'graphql'; -import type { GraphQLQueryFn } from 'graphile-test'; -import { getConnections, seed } from 'graphile-test'; import PostgisPlugin from '../src'; @@ -20,6 +19,7 @@ export const printSchemaOrdered = (originalSchema: GraphQLSchema): string => { const schema = buildASTSchema(parse(printSchema(originalSchema))); const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((name) => { const gqlType = typeMap[name]; @@ -27,6 +27,7 @@ export const printSchemaOrdered = (originalSchema: GraphQLSchema): string => { if ('getFields' in gqlType && typeof gqlType.getFields === 'function') { const fields = gqlType.getFields(); const keys = Object.keys(fields).sort(); + keys.forEach((key) => { const value = fields[key] as { args?: Array<{ name: string }> }; @@ -56,10 +57,12 @@ export const getSchemaSnapshot = async ( query: GraphQLQueryFn ): Promise => { const result = await query(getIntrospectionQuery()); + if (!result.data) { throw new Error('No data returned from introspection query'); } const schema = buildClientSchema(result.data); + return printSchemaOrdered(schema); }; diff --git a/graphile/graphile-search-plugin/__tests__/plugin.test.ts b/graphile/graphile-search-plugin/__tests__/plugin.test.ts index bfc840d60..de7fd6fa5 100644 --- a/graphile/graphile-search-plugin/__tests__/plugin.test.ts +++ b/graphile/graphile-search-plugin/__tests__/plugin.test.ts @@ -1,18 +1,20 @@ import '../test-utils/env'; -import { getConnections, snapshot, seed, GraphQLQueryFn } from 'graphile-test'; + +import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; +// @ts-ignore +import FulltextFilterPlugin from 'graphile-plugin-fulltext-filter'; +import PgSimpleInflector from 'graphile-simple-inflector'; +import { getConnections, GraphQLQueryFn,seed, snapshot } from 'graphile-test'; import { join } from 'path'; import type { PgTestClient } from 'pgsql-test/test-client'; + +import { PgSearchPlugin } from '../src'; import { GoalsSearchViaCondition, - GoalsSearchViaFilter, GoalsSearchViaCondition2, + GoalsSearchViaFilter, GoalsSearchViaFilter2 } from '../test-utils/queries'; -import { PgSearchPlugin } from '../src'; -import PgSimpleInflector from 'graphile-simple-inflector'; -import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; -// @ts-ignore -import FulltextFilterPlugin from 'graphile-plugin-fulltext-filter'; const SCHEMA = 'app_public'; const sql = (f: string) => join(__dirname, '../sql', f); @@ -65,6 +67,7 @@ it('GoalsSearchViaFilter (order not relevant)', async () => { const data = await query(GoalsSearchViaFilter, { search: 'fowl' }); + expect(snapshot(data)).toMatchSnapshot(); }); @@ -72,6 +75,7 @@ it('GoalsSearchViaCondition (order is relevant)', async () => { const data = await query(GoalsSearchViaCondition, { search: 'fowl' }); + expect(snapshot(data)).toMatchSnapshot(); }); @@ -79,6 +83,7 @@ it('GoalsSearchViaFilter2 (order not relevant)', async () => { const data = await query(GoalsSearchViaFilter2, { search: 'fowl' }); + expect(snapshot(data)).toMatchSnapshot(); }); @@ -86,5 +91,6 @@ it('GoalsSearchViaCondition2 (order is relevant)', async () => { const data = await query(GoalsSearchViaCondition2, { search: 'fowl' }); + expect(snapshot(data)).toMatchSnapshot(); }); diff --git a/graphile/graphile-search-plugin/src/index.ts b/graphile/graphile-search-plugin/src/index.ts index 8e34db5b5..133d3b18a 100644 --- a/graphile/graphile-search-plugin/src/index.ts +++ b/graphile/graphile-search-plugin/src/index.ts @@ -1,6 +1,5 @@ // plugins/PgSearchPlugin.ts -import type { Plugin } from 'graphile-build'; export interface PgSearchPluginOptions { /** Prefix for tsvector fields, default is 'tsv' */ @@ -20,17 +19,20 @@ const PgSearchPlugin = (builder: any, options: PgSearchPluginOptions = {}) => { if (!isPgCondition || !table || table.kind !== 'class') return fields; const tsvs = table.attributes.filter((attr: any) => attr.type.name === 'tsvector'); + if (!tsvs.length) return fields; return build.extend( fields, tsvs.reduce((memo: any, attr: any) => { const fieldName = inflection.camelCase(`${pgSearchPrefix}_${attr.name}`); + memo[fieldName] = fieldWithHooks( fieldName, { type: build.graphql.GraphQLString }, {} ); + return memo; }, {}) ); @@ -51,6 +53,7 @@ const PgSearchPlugin = (builder: any, options: PgSearchPluginOptions = {}) => { } = context; const table = tableIfProc || procOrTable; + if ( (!isPgFieldConnection && !isPgFieldSimpleCollection) || !table || @@ -60,6 +63,7 @@ const PgSearchPlugin = (builder: any, options: PgSearchPluginOptions = {}) => { } const tsvs = table.attributes.filter((attr: any) => attr.type.name === 'tsvector'); + if (!tsvs.length) return args; tsvs.forEach((tsv: any) => { @@ -69,6 +73,7 @@ const PgSearchPlugin = (builder: any, options: PgSearchPluginOptions = {}) => { if (!condition || !(conditionFieldName in condition)) return {}; const value = condition[conditionFieldName]; + if (value == null) return {}; return { diff --git a/graphile/graphile-settings/src/index.ts b/graphile/graphile-settings/src/index.ts index 35264135d..0c5b98c56 100644 --- a/graphile/graphile-settings/src/index.ts +++ b/graphile/graphile-settings/src/index.ts @@ -1,22 +1,21 @@ -import PgManyToMany from '@graphile-contrib/pg-many-to-many'; import { getEnvOptions } from '@constructive-io/graphql-env'; import { ConstructiveOptions } from '@constructive-io/graphql-types'; -import PgPostgis from 'graphile-postgis'; -import FulltextFilterPlugin from 'graphile-plugin-fulltext-filter'; +import PgManyToMany from '@graphile-contrib/pg-many-to-many'; import { NodePlugin, Plugin } from 'graphile-build'; import { additionalGraphQLContextFromRequest as langAdditional, LangPlugin } from 'graphile-i18n'; import PgMetaschema from 'graphile-meta-schema'; -import PgSearch from 'graphile-search-plugin'; -import PgSimpleInflector from 'graphile-simple-inflector'; -import { PostGraphileOptions } from 'postgraphile'; +import CustomPgTypeMappingsPlugin from 'graphile-pg-type-mappings'; import ConnectionFilterPlugin from 'graphile-plugin-connection-filter'; import PgPostgisFilter from 'graphile-plugin-connection-filter-postgis'; - -import CustomPgTypeMappingsPlugin from 'graphile-pg-type-mappings'; +import FulltextFilterPlugin from 'graphile-plugin-fulltext-filter'; +import PgPostgis from 'graphile-postgis'; +import PgSearch from 'graphile-search-plugin'; +import PgSimpleInflector from 'graphile-simple-inflector'; import UploadPostGraphilePlugin, { Uploader } from 'graphile-upload-plugin'; +import { PostGraphileOptions } from 'postgraphile'; export const getGraphileSettings = ( rawOpts: ConstructiveOptions @@ -109,6 +108,7 @@ export const getGraphileSettings = ( }, additionalGraphQLContextFromRequest: async (req, res) => { const langContext = await langAdditional(req, res); + return { ...langContext, req, diff --git a/graphile/graphile-simple-inflector/__tests__/index.test.ts b/graphile/graphile-simple-inflector/__tests__/index.test.ts index e193486e4..47cc62374 100644 --- a/graphile/graphile-simple-inflector/__tests__/index.test.ts +++ b/graphile/graphile-simple-inflector/__tests__/index.test.ts @@ -1,5 +1,6 @@ import '../test-utils/env'; -import { GraphQLQueryFn, getConnections, seed, snapshot } from 'graphile-test'; + +import { getConnections, GraphQLQueryFn, seed, snapshot } from 'graphile-test'; import { join } from 'path'; import type { PgTestClient } from 'pgsql-test/test-client'; @@ -45,5 +46,6 @@ afterAll(async () => { it('applies simple inflection', async () => { const data = await query(IntrospectionQuery); + expect(snapshot(data)).toMatchSnapshot(); }); diff --git a/graphile/graphile-simple-inflector/src/index.ts b/graphile/graphile-simple-inflector/src/index.ts index 73a42d70d..f901da653 100644 --- a/graphile/graphile-simple-inflector/src/index.ts +++ b/graphile/graphile-simple-inflector/src/index.ts @@ -107,6 +107,7 @@ export interface PgSimpleInflectorOptions { const fixCapitalisedPlural = (fn: InflectionStringFn): InflectionStringFn => function capitalisedPlural(this: PgInflection, str: string): string { const original = fn.call(this, str); + return original.replace(/[0-9]S(?=[A-Z]|$)/g, (match) => match.toLowerCase()); }; @@ -120,6 +121,7 @@ const fixChangePlural = (fn: InflectionStringFn): InflectionStringFn => const prefix = str.slice(0, index); const word = str.slice(index, suffixIndex); const suffix = str.slice(suffixIndex); + return `${prefix}${fn.call(this, word)}${suffix}`; }; @@ -147,7 +149,7 @@ export const PgSimpleInflector: Plugin = ( pgOmitListSuffix !== true && pgOmitListSuffix !== false ) { - // eslint-disable-next-line no-console + console.warn( 'You can simplify the inflector further by adding `{graphileBuildOptions: {pgOmitListSuffix: true}}` to the options passed to PostGraphile, however be aware that doing so will mean that later enabling relay connections will be a breaking change. To dismiss this message, set `pgOmitListSuffix` to false instead.' ); @@ -179,6 +181,7 @@ export const PgSimpleInflector: Plugin = ( distinctPluralize(this: PgInflection, str: string) { const singular = this.singularize(str); const plural = this.pluralize(singular); + if (singular !== plural) { return plural; } @@ -190,11 +193,12 @@ export const PgSimpleInflector: Plugin = ( plural.endsWith('z') ) { return `${plural}es`; - } else if (plural.endsWith('y')) { + } if (plural.endsWith('y')) { return `${plural.slice(0, -1)}ies`; - } else { - return `${plural}s`; } + + return `${plural}s`; + }, // Fix a naming bug @@ -208,19 +212,23 @@ export const PgSimpleInflector: Plugin = ( const matches = columnName.match( /^(.+?)(_row_id|_id|_uuid|_fk|_pk|RowId|Id|Uuid|UUID|Fk|Pk)$/ ); + if (matches) { return matches[1]; } + return null; }, baseNameMatches(this: PgInflection, baseName: string | null, otherName: string) { const singularizedName = this.singularize(otherName); + return baseName === singularizedName; }, baseNameMatchesAny(this: PgInflection, baseName: string | null, otherName: string) { if (!baseName) return false; + return this.singularize(baseName) === this.singularize(otherName); }, @@ -254,16 +262,19 @@ export const PgSimpleInflector: Plugin = ( if (detailedKeys.length === 1) { const key = detailedKeys[0]; const columnName = this._columnName(key); + return this.getBaseName?.(columnName) ?? null; } if (pgSimplifyMultikeyRelations) { const columnNames = detailedKeys.map((key) => this._columnName(key)); const baseNames = columnNames.map((columnName) => this.getBaseName?.(columnName) ?? null); + // Check none are null if (baseNames.every((n) => n)) { return baseNames.join('-'); } } + return null; }, @@ -318,6 +329,7 @@ export const PgSimpleInflector: Plugin = ( } const baseName = this.getBaseNameFromKeys(detailedKeys); + if (constraint.classId === constraint.foreignClassId) { if (baseName && this.baseNameMatchesAny?.(baseName, table.name)) { return oldInflection.singleRelationByKeys( @@ -336,6 +348,7 @@ export const PgSimpleInflector: Plugin = ( if (this.baseNameMatches?.(baseName, table.name)) { return this.camelCase(`${this._singularizedTableName(table)}`); } + return oldInflection.singleRelationByKeys( detailedKeys, table, @@ -369,6 +382,7 @@ export const PgSimpleInflector: Plugin = ( if (baseName && this.baseNameMatches?.(baseName, foreignTable.name)) { return this.camelCase(`${this._singularizedTableName(table)}`); } + return oldInflection.singleRelationByKeysBackwards( detailedKeys, table, @@ -405,6 +419,7 @@ export const PgSimpleInflector: Plugin = ( `${this.distinctPluralize(this._singularizedTableName(table))}` ); } + return null; }, @@ -418,9 +433,10 @@ export const PgSimpleInflector: Plugin = ( if (constraint.tags.foreignFieldName) { if (constraint.tags.foreignSimpleFieldName) { return constraint.tags.foreignFieldName as string; - } else { - return `${constraint.tags.foreignFieldName}${ConnectionSuffix}`; } + + return `${constraint.tags.foreignFieldName}${ConnectionSuffix}`; + } const base = this._manyRelationByKeysBase?.( detailedKeys, @@ -428,9 +444,11 @@ export const PgSimpleInflector: Plugin = ( foreignTable, constraint ); + if (base) { return base + ConnectionSuffix; } + return ( oldInflection.manyRelationByKeys( detailedKeys, @@ -460,9 +478,11 @@ export const PgSimpleInflector: Plugin = ( foreignTable, constraint ); + if (base) { return base + ListSuffix; } + return ( oldInflection.manyRelationByKeys( detailedKeys, @@ -501,7 +521,8 @@ export const PgSimpleInflector: Plugin = ( if (constraint.type === 'p') { // Primary key, shorten! return this.camelCase(this._singularizedTableName(table)); - } else { + } + return this.camelCase( `${this._singularizedTableName( table @@ -509,7 +530,7 @@ export const PgSimpleInflector: Plugin = ( .map((key) => this.column(key)) .join('-and-')}` ); - } + }, updateByKeys( @@ -526,7 +547,8 @@ export const PgSimpleInflector: Plugin = ( return this.camelCase( `update-${this._singularizedTableName(table)}` ); - } else { + } + return this.camelCase( `update-${this._singularizedTableName( table @@ -534,7 +556,7 @@ export const PgSimpleInflector: Plugin = ( .map((key) => this.column(key)) .join('-and-')}` ); - } + }, deleteByKeys( this: PgInflection, @@ -550,7 +572,8 @@ export const PgSimpleInflector: Plugin = ( return this.camelCase( `delete-${this._singularizedTableName(table)}` ); - } else { + } + return this.camelCase( `delete-${this._singularizedTableName( table @@ -558,7 +581,7 @@ export const PgSimpleInflector: Plugin = ( .map((key) => this.column(key)) .join('-and-')}` ); - } + }, updateByKeysInputType( this: PgInflection, @@ -576,7 +599,8 @@ export const PgSimpleInflector: Plugin = ( return this.upperCamelCase( `update-${this._singularizedTableName(table)}-input` ); - } else { + } + return this.upperCamelCase( `update-${this._singularizedTableName( table @@ -584,7 +608,7 @@ export const PgSimpleInflector: Plugin = ( .map((key) => this.column(key)) .join('-and-')}-input` ); - } + }, deleteByKeysInputType( this: PgInflection, @@ -602,7 +626,8 @@ export const PgSimpleInflector: Plugin = ( return this.upperCamelCase( `delete-${this._singularizedTableName(table)}-input` ); - } else { + } + return this.upperCamelCase( `delete-${this._singularizedTableName( table @@ -610,7 +635,7 @@ export const PgSimpleInflector: Plugin = ( .map((key) => this.column(key)) .join('-and-')}-input` ); - } + }, updateNode(this: PgInflection, table: PgClass) { return this.camelCase( diff --git a/graphile/graphile-test/__tests__/graphile-test.fail.test.ts b/graphile/graphile-test/__tests__/graphile-test.fail.test.ts index 3b24d017c..b859a93eb 100644 --- a/graphile/graphile-test/__tests__/graphile-test.fail.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.fail.test.ts @@ -86,6 +86,7 @@ it('aborts transaction when inserting duplicate usernames', async () => { // Any query run before rollback/commit will throw: // "current transaction is aborted, commands ignored until end of transaction block" const res = await pg.client.query(`select * from app_public.users`); + console.log('After failed tx:', res.rows); // Should not be reached! } catch (err) { const txErr = err as Error; diff --git a/graphile/graphile-test/__tests__/graphile-test.graphile-tx.test.ts b/graphile/graphile-test/__tests__/graphile-test.graphile-tx.test.ts index 7639fdc1f..b500c3eeb 100644 --- a/graphile/graphile-test/__tests__/graphile-test.graphile-tx.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.graphile-tx.test.ts @@ -5,9 +5,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; import type { GraphQLQueryFn } from '../src/types'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; @@ -84,6 +84,7 @@ it('handles duplicate insert via internal PostGraphile savepoint', async () => { // βœ… Step 1: Insert should succeed const first = await query(CREATE_USER, input); + expect(snapshot(first)).toMatchSnapshot('firstInsert'); expect(first.errors).toBeUndefined(); expect(first.data?.createUser?.user?.username).toBe('dupeuser'); @@ -93,12 +94,14 @@ it('handles duplicate insert via internal PostGraphile savepoint', async () => { // So this error will be caught and rolled back to that SAVEPOINT. // The transaction remains clean and usable. const second = await query(CREATE_USER, input); + expect(snapshot(second)).toMatchSnapshot('duplicateInsert'); expect(second.errors?.[0]?.message).toMatch(/duplicate key value/i); expect(second.data?.createUser).toBeNull(); // βœ… Step 3: Query still works β€” transaction was not aborted const followup = await query(GET_USERS); + expect(snapshot(followup)).toMatchSnapshot('queryAfterDuplicateInsert'); expect(followup.errors).toBeUndefined(); expect(followup.data?.allUsers?.nodes.some((u: any) => u.username === 'dupeuser')).toBe(true); diff --git a/graphile/graphile-test/__tests__/graphile-test.graphql.test.ts b/graphile/graphile-test/__tests__/graphile-test.graphql.test.ts index 9aeb72cb1..16c03fe1c 100644 --- a/graphile/graphile-test/__tests__/graphile-test.graphql.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.graphql.test.ts @@ -5,9 +5,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; import type { GraphQLQueryFn } from '../src/types'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; diff --git a/graphile/graphile-test/__tests__/graphile-test.plugins.test.ts b/graphile/graphile-test/__tests__/graphile-test.plugins.test.ts index 986bc11e2..d90fe4005 100644 --- a/graphile/graphile-test/__tests__/graphile-test.plugins.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.plugins.test.ts @@ -16,6 +16,7 @@ const sql = (f: string) => join(__dirname, '/../sql', f); const TestPlugin = (builder: any) => { builder.hook('GraphQLObjectType:fields', (fields: any, build: any, context: any) => { const { scope } = context; + if (scope.isRootQuery) { return build.extend(fields, { testPluginField: { @@ -24,6 +25,7 @@ const TestPlugin = (builder: any) => { } }); } + return fields; }); }; @@ -32,6 +34,7 @@ const TestPlugin = (builder: any) => { const AnotherTestPlugin = (builder: any) => { builder.hook('GraphQLObjectType:fields', (fields: any, build: any, context: any) => { const { scope } = context; + if (scope.isRootQuery) { return build.extend(fields, { anotherTestField: { @@ -40,6 +43,7 @@ const AnotherTestPlugin = (builder: any) => { } }); } + return fields; }); }; @@ -82,22 +86,26 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); it('should include plugin field in introspection', async () => { const res = await query(IntrospectionQuery); + expect(res.data).not.toBeNull(); expect(res.data).not.toBeUndefined(); expect(res.errors).toBeUndefined(); const queryTypeName = res.data?.__schema?.queryType?.name; + expect(queryTypeName).toBe('Query'); // Find the Query type in the types array const types = res.data?.__schema?.types || []; const queryType = types.find((t: any) => t.name === queryTypeName); + expect(queryType).not.toBeNull(); expect(queryType).not.toBeUndefined(); expect(queryType?.name).toBe('Query'); @@ -105,6 +113,7 @@ describe('graphile-test with plugins', () => { const fields = queryType?.fields || []; const testField = fields.find((f: any) => f.name === 'testPluginField'); + expect(testField).not.toBeNull(); expect(testField).not.toBeUndefined(); expect(testField?.name).toBe('testPluginField'); @@ -113,6 +122,7 @@ describe('graphile-test with plugins', () => { const typeName = testField.type?.name || testField.type?.ofType?.name || testField.type?.ofType?.ofType?.name; + expect(typeName).toBe('String'); }); }); @@ -155,6 +165,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.data?.anotherTestField).toBe('another-test-value'); expect(res.errors).toBeUndefined(); @@ -202,6 +213,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); @@ -249,6 +261,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); @@ -298,6 +311,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.data?.anotherTestField).toBe('another-test-value'); expect(res.errors).toBeUndefined(); diff --git a/graphile/graphile-test/__tests__/graphile-test.roles.test.ts b/graphile/graphile-test/__tests__/graphile-test.roles.test.ts index 5a1f0b959..4d6e3d6ed 100644 --- a/graphile/graphile-test/__tests__/graphile-test.roles.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.roles.test.ts @@ -5,9 +5,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; import type { GraphQLQueryFn } from '../src/types'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; diff --git a/graphile/graphile-test/__tests__/graphile-test.test.ts b/graphile/graphile-test/__tests__/graphile-test.test.ts index 729355721..3ed999238 100644 --- a/graphile/graphile-test/__tests__/graphile-test.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.test.ts @@ -4,7 +4,6 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; import type { GraphQLQueryFn } from '../src/types'; import { IntrospectionQuery } from '../test-utils/queries'; @@ -55,5 +54,6 @@ it('introspection query works', async () => { // Should have allUsers field (default PostGraphile behavior) const allUsersField = fields.find((f: any) => f.name === 'allUsers'); + expect(allUsersField).toBeDefined(); }); diff --git a/graphile/graphile-test/__tests__/graphile-test.types.positional.test.ts b/graphile/graphile-test/__tests__/graphile-test.types.positional.test.ts index f651e80b8..da44f3696 100644 --- a/graphile/graphile-test/__tests__/graphile-test.types.positional.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.types.positional.test.ts @@ -4,9 +4,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; import type { GraphQLQueryFn } from '../src/types'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphile/graphile-test/__tests__/graphile-test.types.positional.unwrapped.test.ts b/graphile/graphile-test/__tests__/graphile-test.types.positional.unwrapped.test.ts index eff7f6cef..e83040696 100644 --- a/graphile/graphile-test/__tests__/graphile-test.types.positional.unwrapped.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.types.positional.unwrapped.test.ts @@ -4,9 +4,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsUnwrapped } from '../src/get-connections'; import type { GraphQLQueryUnwrappedFn } from '../src/types'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphile/graphile-test/__tests__/graphile-test.types.test.ts b/graphile/graphile-test/__tests__/graphile-test.types.test.ts index d2865867d..23fd41792 100644 --- a/graphile/graphile-test/__tests__/graphile-test.types.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.types.test.ts @@ -4,9 +4,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsObject } from '../src/get-connections'; import type { GraphQLQueryFnObj } from '../src/types'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphile/graphile-test/__tests__/graphile-test.types.unwrapped.test.ts b/graphile/graphile-test/__tests__/graphile-test.types.unwrapped.test.ts index 81a84eeb4..be170b8a1 100644 --- a/graphile/graphile-test/__tests__/graphile-test.types.unwrapped.test.ts +++ b/graphile/graphile-test/__tests__/graphile-test.types.unwrapped.test.ts @@ -4,9 +4,9 @@ import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsObjectUnwrapped } from '../src/get-connections'; import type { GraphQLQueryUnwrappedFnObj } from '../src/types'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphile/graphile-test/src/clean.ts b/graphile/graphile-test/src/clean.ts index 821309765..fcf8920f6 100644 --- a/graphile/graphile-test/src/clean.ts +++ b/graphile/graphile-test/src/clean.ts @@ -11,6 +11,7 @@ function mapValues( ): Record { return Object.entries(obj).reduce((acc, [key, value]) => { acc[key as keyof T] = fn(value, key as keyof T); + return acc; }, {} as Record); } @@ -22,13 +23,14 @@ export const pruneDates = (row: AnyObject): AnyObject => } if (v instanceof Date) { return '[DATE]'; - } else if ( + } if ( typeof v === 'string' && /(_at|At)$/.test(k as string) && /^20[0-9]{2}-[0-9]{2}-[0-9]{2}/.test(v) ) { return '[DATE]'; } + return v; }); @@ -58,6 +60,7 @@ export const pruneUUIDs = (row: AnyObject): AnyObject => if (k === 'gravatar' && /^[0-9a-f]{32}$/i.test(v)) { return '[gUUID]'; } + return v; }); @@ -77,8 +80,9 @@ export const prune = (obj: AnyObject): AnyObject => export const snapshot = (obj: unknown): unknown => { if (Array.isArray(obj)) { return obj.map(snapshot); - } else if (obj && typeof obj === 'object') { + } if (obj && typeof obj === 'object') { return mapValues(prune(obj as AnyObject), snapshot); } + return obj; }; diff --git a/graphile/graphile-test/src/context.ts b/graphile/graphile-test/src/context.ts index 4bb4964b8..cf51991e1 100644 --- a/graphile/graphile-test/src/context.ts +++ b/graphile/graphile-test/src/context.ts @@ -72,6 +72,7 @@ export const runGraphQLInContext = async ({ const pgConn = input.useRoot ? conn.pg : conn.db; const pgClient = pgConn.client; + // IS THIS BAD TO HAVE ROLE HERE await setContextOnClient(pgClient, pgSettings, authRole); await pgConn.ctxQuery(); @@ -87,6 +88,7 @@ export const runGraphQLInContext = async ({ contextValue: { ...context, ...additionalContext, pgClient }, variableValues: variables ?? null }); + return result as T; } ); diff --git a/graphile/graphile-test/src/get-connections.ts b/graphile/graphile-test/src/get-connections.ts index 6c8ca72c7..eb4504327 100644 --- a/graphile/graphile-test/src/get-connections.ts +++ b/graphile/graphile-test/src/get-connections.ts @@ -22,6 +22,7 @@ const unwrap = (res: GraphQLResponse): T => { if (!res.data) { throw new Error('No data returned from GraphQL query'); } + return res.data; }; @@ -34,6 +35,7 @@ const createConnectionsBase = async ( const { pg, db, teardown: dbTeardown } = conn; const gqlContext = GraphQLTest(input, conn); + await gqlContext.setup(); const teardown = async () => { @@ -124,7 +126,9 @@ export const getConnectionsObjectWithLogging = async ( const query: GraphQLQueryFnObj = async (opts) => { console.log('Executing GraphQL query:', opts.query); const result = await baseQuery(opts); + console.log('GraphQL result:', result); + return result; }; @@ -154,7 +158,9 @@ export const getConnectionsObjectWithTiming = async ( const start = Date.now(); const result = await baseQuery(opts); const duration = Date.now() - start; + console.log(`GraphQL query took ${duration}ms`); + return result; }; @@ -234,7 +240,9 @@ export const getConnectionsWithLogging = async ( const query: GraphQLQueryFn = async (query, variables, commit, reqOptions) => { console.log('Executing positional GraphQL query:', query); const result = await baseQueryPositional(query, variables, commit, reqOptions); + console.log('GraphQL result:', result); + return result; }; @@ -264,7 +272,9 @@ export const getConnectionsWithTiming = async ( const start = Date.now(); const result = await baseQueryPositional(query, variables, commit, reqOptions); const duration = Date.now() - start; + console.log(`Positional GraphQL query took ${duration}ms`); + return result; }; diff --git a/graphile/graphile-test/test-utils/utils.ts b/graphile/graphile-test/test-utils/utils.ts index bef9b854f..b792c423d 100644 --- a/graphile/graphile-test/test-utils/utils.ts +++ b/graphile/graphile-test/test-utils/utils.ts @@ -6,6 +6,7 @@ export const logDbSessionInfo = async (db: { query: (sql: string) => Promise join(__dirname, '../sql', f); @@ -66,8 +67,10 @@ jest.setTimeout(3000000); const testFiles: string[] = []; const createTestFile = (filename: string, content: string): string => { const filePath = join(tmpdir(), `upload-test-${Date.now()}-${filename}`); + writeFileSync(filePath, content); testFiles.push(filePath); + return filePath; }; @@ -83,6 +86,7 @@ let db: PgTestClient; beforeAll(async () => { process.env.IS_MINIO = 'true'; // Ensure MinIO behavior in createS3Bucket const result = await createS3Bucket(s3Client, BUCKET_NAME!); + if (!result.success) throw new Error('Failed to create test S3 bucket'); // Initialize uploader with real S3 configuration @@ -198,6 +202,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const avatarField = inputFields.find( (f: any) => f.name === 'avatarUrlUpload' ); + expect(avatarField?.type?.name).toBe('Upload'); expect(data.errors).toBeUndefined(); }); @@ -226,6 +231,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { pluginUploadFields.forEach((fieldName) => { const field = inputFields.find((f: any) => f.name === fieldName); + expect(field).toBeDefined(); expect(field?.type?.name).toBe('Upload'); }); @@ -367,6 +373,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(data.errors).toBeUndefined(); const document = data.data?.createDocument?.document; + expect(document).toMatchObject({ title: 'Test Document', }); @@ -390,6 +397,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const document = createData.data?.createDocument?.document; const documentId = document?.id; + expect(documentId).toBeDefined(); // Get nodeId from the created document @@ -403,6 +411,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { `; const documentData = await query(getDocumentQuery, { id: documentId }); const nodeId = documentData.data?.documentById?.nodeId; + expect(nodeId).toBeDefined(); // Create new file @@ -418,7 +427,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const updateData = await query(UpdateDocumentWithUpload, { input: { - nodeId: nodeId, + nodeId, documentPatch: { fileUploadUpload: { promise: uploadPromise }, }, @@ -427,6 +436,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(updateData.errors).toBeUndefined(); const updatedDocument = updateData.data?.updateDocument?.document; + expect(updatedDocument).toMatchObject({ id: documentId, title: 'Original Document', @@ -440,6 +450,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const fileUploadData = typeof updatedDocument.fileUpload === 'string' ? JSON.parse(updatedDocument.fileUpload) : updatedDocument.fileUpload; + expect(fileUploadData).toHaveProperty('url'); } @@ -469,6 +480,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(data.errors).toBeUndefined(); const media = data.data?.createMedia?.media; + expect(media).toMatchObject({ name: 'Test Media', }); @@ -481,6 +493,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const uploadData = typeof media.uploadData === 'string' ? JSON.parse(media.uploadData) : media.uploadData; + expect(uploadData).toHaveProperty('url'); } @@ -544,6 +557,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(data.errors).toBeUndefined(); const product = data.data?.createProduct?.product; + expect(product).toMatchObject({ name: 'Test Product', }); @@ -591,6 +605,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { // Verify all are Upload type ['avatarUpload', 'resumeUpload', 'portfolioUpload', 'customDataUpload'].forEach((fieldName) => { const field = inputFields.find((f: any) => f.name === fieldName); + expect(field).toBeDefined(); expect(field?.type?.name).toBe('Upload'); }); @@ -637,6 +652,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(data.errors).toBeUndefined(); const profile = data.data?.createProfile?.profile; + expect(profile).toMatchObject({ userId: 1, }); @@ -651,6 +667,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const avatarData = typeof profile.avatar === 'string' ? JSON.parse(profile.avatar) : profile.avatar; + expect(avatarData).toHaveProperty('url'); } if (profile?.resume) { @@ -663,6 +680,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { const portfolioData = typeof profile.portfolio === 'string' ? JSON.parse(profile.portfolio) : profile.portfolio; + expect(portfolioData).toHaveProperty('url'); } }); @@ -697,6 +715,7 @@ describe('UploadPostGraphilePlugin - Core Capabilities', () => { expect(data.errors).toBeUndefined(); const document = data.data?.createDocument?.document; + expect(document).toMatchObject({ title: 'Document with Attachment', }); diff --git a/graphile/graphile-upload-plugin/src/index.ts b/graphile/graphile-upload-plugin/src/index.ts index 0e9b2ee7c..bdc3f2fa5 100644 --- a/graphile/graphile-upload-plugin/src/index.ts +++ b/graphile/graphile-upload-plugin/src/index.ts @@ -1,16 +1,16 @@ import UploadPostGraphilePlugin, { type FileUpload, + type UploadFieldDefinition, type UploadPluginInfo, type UploadResolver, - type UploadFieldDefinition, } from './plugin'; export { - UploadPostGraphilePlugin, type FileUpload, + type UploadFieldDefinition, type UploadPluginInfo, + UploadPostGraphilePlugin, type UploadResolver, - type UploadFieldDefinition, }; export { Uploader, type UploaderOptions } from './resolvers/upload'; diff --git a/graphile/graphile-upload-plugin/src/plugin.ts b/graphile/graphile-upload-plugin/src/plugin.ts index afcd160dd..5fc2a2fd4 100644 --- a/graphile/graphile-upload-plugin/src/plugin.ts +++ b/graphile/graphile-upload-plugin/src/plugin.ts @@ -56,11 +56,13 @@ const UploadPostGraphilePlugin: Plugin = ( attr.type?.namespaceName === namespaceName) || (tag && attr.tags?.[tag]) ); + if (types.length === 1) { return types[0]; - } else if (types.length > 1) { + } if (types.length > 1) { throw new Error('Upload field definitions are ambiguous'); } + return undefined; }; @@ -76,6 +78,7 @@ const UploadPostGraphilePlugin: Plugin = ( parseValue(value: unknown) { // The value should be an object with a `.promise` that resolves to the file upload const maybe = value as any; + if ( maybe && maybe.promise && @@ -101,6 +104,7 @@ const UploadPostGraphilePlugin: Plugin = ( const theType = build.pgIntrospectionResultsByKind.type.find( (typ: any) => typ.name === name && typ.namespaceName === namespaceName ); + if (theType) { build.pgRegisterGqlTypeByTypeId(theType.id, () => build.getTypeByName(type) @@ -115,7 +119,7 @@ const UploadPostGraphilePlugin: Plugin = ( return build.extend(inflection, { // NO ARROW FUNCTIONS HERE (this) uploadColumn(this: any, attr: any) { - return this.column(attr) + 'Upload'; + return `${this.column(attr) }Upload`; }, }); }); @@ -141,6 +145,7 @@ const UploadPostGraphilePlugin: Plugin = ( : context.scope.isPgPatch ? 'update' : 'create'; + if (build.pgOmit(attr, action)) return memo; if (attr.identity === 'a') return memo; @@ -176,6 +181,7 @@ const UploadPostGraphilePlugin: Plugin = ( } )}` ); + return memo; }, {}), `Adding columns to '${build.describePgEntity(table)}'` @@ -216,14 +222,17 @@ const UploadPostGraphilePlugin: Plugin = ( (memo: Record, attr: any) => { // first, try to directly match the types here const typeMatched = relevantUploadType(attr); + if (typeMatched) { const fieldName = inflection.column(attr); const uploadFieldName = inflection.uploadColumn(attr); + memo[uploadFieldName] = typeMatched.resolve; tags[uploadFieldName] = attr.tags; types[uploadFieldName] = attr.type.name; originals[uploadFieldName] = fieldName; } + return memo; }, {} as Record @@ -248,7 +257,8 @@ const UploadPostGraphilePlugin: Plugin = ( if (obj[key] instanceof Promise) { if (uploadResolversByFieldName[key]) { const upload = await obj[key]; - // eslint-disable-next-line require-atomic-updates + + obj[originals[key]] = await uploadResolversByFieldName[key]( upload, args, @@ -272,6 +282,7 @@ const UploadPostGraphilePlugin: Plugin = ( context, info ); + // Finally return the result. return oldResolveResult; }, diff --git a/graphql/codegen/__tests__/codegen.fixtures.test.ts b/graphql/codegen/__tests__/codegen.fixtures.test.ts index 813fb4d24..e19c820f7 100644 --- a/graphql/codegen/__tests__/codegen.fixtures.test.ts +++ b/graphql/codegen/__tests__/codegen.fixtures.test.ts @@ -30,6 +30,7 @@ describe('GraphQL Code Generation', () => { it('generate(): full AST map snapshot', () => { // @ts-ignore const output = generateKeyedObjFromGqlMap({ ...queries, ...mutations }); + expect(output).toMatchSnapshot('full generate() output'); }); diff --git a/graphql/codegen/__tests__/codegen.test.ts b/graphql/codegen/__tests__/codegen.test.ts index 629dd4b60..950c5bc30 100644 --- a/graphql/codegen/__tests__/codegen.test.ts +++ b/graphql/codegen/__tests__/codegen.test.ts @@ -43,10 +43,12 @@ it('generates output', () => { const gen = generate(gqlMap); const output = Object.keys(gen).reduce>((acc, key) => { const entry = gen[key]; + if (entry?.ast) { // @ts-ignore acc[key] = print(entry.ast); } + return acc; }, {}); @@ -57,5 +59,6 @@ it('generates output', () => { it('helper method', () => { expect(introspection).toBeDefined(); const output = generateKeyedObjFromIntrospection(introspection); + expect(output).toMatchSnapshot(); }); diff --git a/graphql/codegen/__tests__/gql.mutations.test.ts b/graphql/codegen/__tests__/gql.mutations.test.ts index 03287cc68..c5d85e360 100644 --- a/graphql/codegen/__tests__/gql.mutations.test.ts +++ b/graphql/codegen/__tests__/gql.mutations.test.ts @@ -1,5 +1,6 @@ import { print } from 'graphql' -import { createOne, patchOne, deleteOne, createMutation, MutationSpec } from '../src' + +import { createMutation, createOne, deleteOne, MutationSpec,patchOne } from '../src' describe('gql mutation builders', () => { it('createMutation: builds non-null vars and selects scalar outputs', () => { @@ -20,8 +21,10 @@ describe('gql mutation builders', () => { } const res = createMutation({ operationName: 'customMutation', mutation }) + expect(res).toBeDefined() const s = print(res!.ast) + expect(s).toContain('mutation customMutationMutation') expect(s).toContain('$foo: String!') expect(s).toContain('$list: [Int!]!') @@ -47,8 +50,10 @@ describe('gql mutation builders', () => { } const res = createOne({ operationName: 'createAction', mutation }) + expect(res).toBeDefined() const s = print(res!.ast) + expect(s).toContain('mutation createActionMutation') expect(s).toContain('$name: String!') expect(s).not.toContain('$createdAt') @@ -75,8 +80,10 @@ describe('gql mutation builders', () => { } const res = patchOne({ operationName: 'patchAction', mutation }) + expect(res).toBeDefined() const s = print(res!.ast) + expect(s).toContain('mutation patchActionMutation') expect(s).toContain('$id: UUID!') expect(s).toContain('$name: String') @@ -99,8 +106,10 @@ describe('gql mutation builders', () => { } const res = deleteOne({ operationName: 'deleteAction', mutation }) + expect(res).toBeDefined() const s = print(res!.ast) + expect(s).toContain('mutation deleteActionMutation') expect(s).toContain('$id: UUID!') expect(s).toContain('$tags: [String]!') diff --git a/graphql/codegen/__tests__/granular.test.ts b/graphql/codegen/__tests__/granular.test.ts index 84f9ebc09..0b0cfee80 100644 --- a/graphql/codegen/__tests__/granular.test.ts +++ b/graphql/codegen/__tests__/granular.test.ts @@ -17,7 +17,9 @@ it('generate', () => { // @ts-ignore m[key] = print(gen[key].ast); } + return m; }, {}); + expect(output).toMatchSnapshot(); }); diff --git a/graphql/codegen/__tests__/options.test.ts b/graphql/codegen/__tests__/options.test.ts index ffccaa35a..da1ae2b72 100644 --- a/graphql/codegen/__tests__/options.test.ts +++ b/graphql/codegen/__tests__/options.test.ts @@ -39,6 +39,7 @@ describe('mergeGraphQLCodegenOptions', () => { const base = { ...defaultGraphQLCodegenOptions } const overrides = { reactQuery: { fetcher: 'fetch', legacyMode: true, exposeDocument: true } } as any const merged = mergeGraphQLCodegenOptions(base, overrides) + expect((merged as any).reactQuery).toBeUndefined() // base is not mutated expect(base.reactQuery?.fetcher).toBe('graphql-request') diff --git a/graphql/codegen/src/codegen.ts b/graphql/codegen/src/codegen.ts index 8e1bfb7ff..b75752b65 100644 --- a/graphql/codegen/src/codegen.ts +++ b/graphql/codegen/src/codegen.ts @@ -1,19 +1,21 @@ import { promises as fs } from 'fs' -import { join, dirname, isAbsolute, resolve } from 'path' -import { buildSchema, buildClientSchema, graphql, getIntrospectionQuery, print } from 'graphql' +import { buildClientSchema, buildSchema, getIntrospectionQuery, graphql, print } from 'graphql' +import { dirname, isAbsolute, join, resolve } from 'path' const inflection: any = require('inflection') -import { generate as generateGql, GqlMap } from './gql' -import { parseGraphQuery } from 'introspectron' -import { defaultGraphQLCodegenOptions, GraphQLCodegenOptions, mergeGraphQLCodegenOptions } from './options' + +import generate from '@babel/generator' +import { parse } from '@babel/parser' +import * as t from '@babel/types' import { codegen as runCoreCodegen } from '@graphql-codegen/core' import * as typescriptPlugin from '@graphql-codegen/typescript' -import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations' import * as typescriptGraphqlRequestPlugin from '@graphql-codegen/typescript-graphql-request' +import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations' import * as typescriptReactQueryPlugin from '@graphql-codegen/typescript-react-query' import { GraphQLClient } from 'graphql-request' -import { parse } from '@babel/parser' -import generate from '@babel/generator' -import * as t from '@babel/types' +import { parseGraphQuery } from 'introspectron' + +import { generate as generateGql, GqlMap } from './gql' +import { defaultGraphQLCodegenOptions, GraphQLCodegenOptions, mergeGraphQLCodegenOptions } from './options' function addDocumentNodeImport(code: string): string { const ast = parse(code, { @@ -25,11 +27,13 @@ function addDocumentNodeImport(code: string): string { [t.importSpecifier(t.identifier('DocumentNode'), t.identifier('DocumentNode'))], t.stringLiteral('graphql') ) + importDecl.importKind = 'type' ast.program.body.unshift(importDecl) const output = generate(ast, {}, code) + return output.code } @@ -37,6 +41,7 @@ function getFilename(key: string, convention: GraphQLCodegenOptions['documents'] if (convention === 'underscore') return inflection.underscore(key) if (convention === 'dashed') return inflection.underscore(key).replace(/_/g, '-') if (convention === 'camelUpper') return inflection.camelize(key, false) + return key } @@ -58,6 +63,7 @@ async function getIntrospectionFromSDL(schemaPath: string) { const schema = buildSchema(sdl) const q = getIntrospectionQuery() const res = await graphql({ schema, source: q }) + return res.data as any } @@ -65,13 +71,16 @@ async function getIntrospectionFromEndpoint(endpoint: string, headers?: Record(q) + return res as any } function generateKeyedObjFromGqlMap(gqlMap: GqlMap): Record { const gen = generateGql(gqlMap) + return Object.entries(gen).reduce>((acc, [key, val]) => { if (val?.ast) acc[key] = print(val.ast) + return acc }, {}) } @@ -82,6 +91,7 @@ function applyQueryFilters(map: GqlMap, docs: GraphQLCodegenOptions['documents'] const patterns = (docs.excludePatterns || []).filter(Boolean) let keys = Object.keys(map) + if (allow.length > 0) keys = keys.filter((k) => allow.includes(k)) if (exclude.length > 0) keys = keys.filter((k) => !exclude.includes(k)) if (patterns.length > 0) { @@ -92,11 +102,13 @@ function applyQueryFilters(map: GqlMap, docs: GraphQLCodegenOptions['documents'] return null } }).filter((r): r is RegExp => !!r) + keys = keys.filter((k) => !regs.some((r) => r.test(k))) } return keys.reduce((acc, k) => { acc[k] = map[k] + return acc }, {}) } @@ -104,10 +116,13 @@ function applyQueryFilters(map: GqlMap, docs: GraphQLCodegenOptions['documents'] async function writeOperationsDocuments(docs: Record, dir: string, format: 'gql' | 'ts', convention: GraphQLCodegenOptions['documents']['convention']) { await ensureDir(dir) const index: string[] = [] + for (const key of Object.keys(docs)) { const filename = getFilename(key, convention) + (format === 'ts' ? '.ts' : '.gql') + if (format === 'ts') { const code = `import gql from 'graphql-tag'\nexport const ${key} = gql\`\n${docs[key]}\n\`` + await writeFileUTF8(join(dir, filename), code) index.push(`export * from './${filename}'`) } else { @@ -162,14 +177,17 @@ export async function runCodegen(opts: GraphQLCodegenOptions, cwd: string) { plugins: [{ typescript: {} }], pluginMap: { typescript: typescriptPlugin as any } }) + await writeFileUTF8(typesFile, typesContent) } if (options.features.emitSdk) { const documents: { location: string; document: any }[] = [] + for (const [name, content] of Object.entries(docs)) { try { const doc = require('graphql').parse(content) + documents.push({ location: name, document: doc }) } catch (e) {} } @@ -191,14 +209,17 @@ export async function runCodegen(opts: GraphQLCodegenOptions, cwd: string) { }) // Fix TS2742: Add missing DocumentNode import using Babel AST const sdkContentWithImport = addDocumentNodeImport(sdkContent) + await writeFileUTF8(sdkFile, sdkContentWithImport) } if (options.features.emitReactQuery) { const documents: { location: string; document: any }[] = [] + for (const [name, content] of Object.entries(docs)) { try { const doc = require('graphql').parse(content) + documents.push({ location: name, document: doc }) } catch (e) {} } @@ -224,6 +245,7 @@ export async function runCodegen(opts: GraphQLCodegenOptions, cwd: string) { 'typescript-react-query': typescriptReactQueryPlugin as any } }) + await writeFileUTF8(reactQueryFile, rqContent) } @@ -234,11 +256,13 @@ export async function runCodegenFromJSONConfig(configPath: string, cwd: string) const path = isAbsolute(configPath) ? configPath : resolve(cwd, configPath) const content = await readFileUTF8(path) let overrides: any = {} + try { overrides = JSON.parse(content) } catch (e) { - throw new Error('Invalid JSON config: ' + e) + throw new Error(`Invalid JSON config: ${ e}`) } const merged = mergeGraphQLCodegenOptions(defaultGraphQLCodegenOptions, overrides as any) + return runCodegen(merged as GraphQLCodegenOptions, cwd) } diff --git a/graphql/codegen/src/gql.ts b/graphql/codegen/src/gql.ts index a31809cd2..f7a78aa79 100644 --- a/graphql/codegen/src/gql.ts +++ b/graphql/codegen/src/gql.ts @@ -576,7 +576,8 @@ export const createOne = ({ ); if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for ' + mutationName); + console.log(`no input field for mutation for ${ mutationName}`); + return; } @@ -691,7 +692,8 @@ export const patchOne = ({ ); if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for ' + mutationName); + console.log(`no input field for mutation for ${ mutationName}`); + return; } @@ -743,6 +745,7 @@ export const patchOne = ({ const patchByVarDefs: VariableDefinitionNode[] = patchByAttrs.map(({ name, type, isNotNull, isArray, isArrayNotNull }) => { let gqlType: TypeNode = t.namedType({ type }); + if (isNotNull) { gqlType = t.nonNullType({ type: gqlType }); } @@ -752,6 +755,7 @@ export const patchOne = ({ gqlType = t.nonNullType({ type: gqlType }); } } + return t.variableDefinition({ variable: t.variable({ name }), type: gqlType }); }); @@ -823,7 +827,8 @@ export const deleteOne = ({ ); if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for ' + mutationName); + console.log(`no input field for mutation for ${ mutationName}`); + return; } @@ -907,7 +912,8 @@ export const createMutation = ({ ); if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for ' + mutationName); + console.log(`no input field for mutation for ${ mutationName}`); + return; } @@ -1040,6 +1046,7 @@ export const generate = (gql: GqlMap): AstMap => { ].forEach(fn => { // @ts-ignore const result = fn({ operationName, query: defn }); + if (result?.name && result?.ast) { m[result.name] = result; } @@ -1048,7 +1055,7 @@ export const generate = (gql: GqlMap): AstMap => { // @ts-ignore ({ name, ast } = getOne({ operationName, query: defn }) ?? {}); } else { - console.warn('Unknown qtype for key: ' + operationName); + console.warn(`Unknown qtype for key: ${ operationName}`); } if (name && ast) { @@ -1073,6 +1080,7 @@ export const generateGranular = ( if (defn.qtype === 'getMany') { const many = getMany({ operationName, query: defn, fields }); + if (many?.name && many?.ast && model === matchModel) { m[many.name] = many; } @@ -1082,6 +1090,7 @@ export const generateGranular = ( query: defn, fields, }); + if (paginatedEdges?.name && paginatedEdges?.ast && model === matchModel) { m[paginatedEdges.name] = paginatedEdges; } @@ -1091,11 +1100,13 @@ export const generateGranular = ( query: defn, fields, }); + if (paginatedNodes?.name && paginatedNodes?.ast && model === matchModel) { m[paginatedNodes.name] = paginatedNodes; } } else if (defn.qtype === 'getOne') { const one = getOne({ operationName, query: defn, fields }); + if (one?.name && one?.ast && model === matchModel) { m[one.name] = one; } @@ -1115,6 +1126,7 @@ export function getSelections( const mapItem = (item: QueryField): FieldNode | null => { if (typeof item === 'string') { if (!useAll && !fields.includes(item)) return null; + return t.field({ name: item }); } if ( @@ -1126,6 +1138,7 @@ export function getSelections( ) { if (!useAll && !fields.includes(item.name)) return null; const isMany = (item as any).qtype === 'getMany'; + if (isMany) { return t.field({ name: item.name, @@ -1142,11 +1155,13 @@ export function getSelections( }), }); } + return t.field({ name: item.name, selectionSet: t.selectionSet({ selections: item.selection.map((s) => mapItem(s)).filter(Boolean) as FieldNode[] }), }); } + return null; }; diff --git a/graphql/codegen/src/index.ts b/graphql/codegen/src/index.ts index e1c679be9..56e057e46 100644 --- a/graphql/codegen/src/index.ts +++ b/graphql/codegen/src/index.ts @@ -1,3 +1,3 @@ -export * from './gql' export * from './codegen' +export * from './gql' export * from './options' diff --git a/graphql/codegen/test-utils/generate-from-introspection.ts b/graphql/codegen/test-utils/generate-from-introspection.ts index 7e435b5e9..56f8a109d 100644 --- a/graphql/codegen/test-utils/generate-from-introspection.ts +++ b/graphql/codegen/test-utils/generate-from-introspection.ts @@ -10,6 +10,7 @@ export function generateKeyedObjFromGqlMap(gqlMap: GqlMap): Record { const { queries, mutations } = parseGraphQuery(introspection); const gqlMap: GqlMap = { ...queries, ...mutations }; + return generateKeyedObjFromGqlMap(gqlMap); } \ No newline at end of file diff --git a/graphql/env/src/env.ts b/graphql/env/src/env.ts index 976238d16..f7a0020b9 100644 --- a/graphql/env/src/env.ts +++ b/graphql/env/src/env.ts @@ -6,6 +6,7 @@ import { ConstructiveOptions } from '@constructive-io/graphql-types'; */ const parseEnvBoolean = (val?: string): boolean | undefined => { if (val === undefined) return undefined; + return ['true', '1', 'yes'].includes(val.toLowerCase()); }; diff --git a/graphql/env/src/index.ts b/graphql/env/src/index.ts index e88ecd170..b3e25f6e8 100644 --- a/graphql/env/src/index.ts +++ b/graphql/env/src/index.ts @@ -1,18 +1,17 @@ // Re-export core env utilities from @pgpmjs/env export { - loadConfigSync, - loadConfigFileSync, - loadConfigSyncFromDir, - resolvePgpmPath, getConnEnvOptions, getDeploymentEnvOptions, - getNodeEnv -} from '@pgpmjs/env'; + getNodeEnv, + loadConfigFileSync, + loadConfigSync, + loadConfigSyncFromDir, + resolvePgpmPath} from '@pgpmjs/env'; // Export Constructive-specific env functions -export { getEnvOptions, getConstructiveEnvOptions } from './merge'; export { getGraphQLEnvVars } from './env'; +export { getConstructiveEnvOptions,getEnvOptions } from './merge'; // Re-export types for convenience -export type { ConstructiveOptions, ConstructiveGraphQLOptions } from '@constructive-io/graphql-types'; +export type { ConstructiveGraphQLOptions,ConstructiveOptions } from '@constructive-io/graphql-types'; export { constructiveDefaults, constructiveGraphqlDefaults } from '@constructive-io/graphql-types'; diff --git a/graphql/env/src/merge.ts b/graphql/env/src/merge.ts index d82352f75..3d1d409dd 100644 --- a/graphql/env/src/merge.ts +++ b/graphql/env/src/merge.ts @@ -1,6 +1,7 @@ -import deepmerge from 'deepmerge'; -import { ConstructiveOptions, constructiveGraphqlDefaults } from '@constructive-io/graphql-types'; +import { constructiveGraphqlDefaults,ConstructiveOptions } from '@constructive-io/graphql-types'; import { getEnvOptions as getPgpmEnvOptions, loadConfigSync } from '@pgpmjs/env'; +import deepmerge from 'deepmerge'; + import { getGraphQLEnvVars } from './env'; /** diff --git a/graphql/explorer/src/render.ts b/graphql/explorer/src/render.ts index 0ef022864..127cd73ed 100644 --- a/graphql/explorer/src/render.ts +++ b/graphql/explorer/src/render.ts @@ -72,6 +72,7 @@ export const printDatabases = ({ databases, req, port }: PrintDatabasesParams): const links = databases .map((d) => { const url = `${req.protocol}://${d.datname}.${req.hostname}:${port}`; + return `${d.datname}`; }) .join(''); @@ -91,6 +92,7 @@ export const printSchemas = ({ dbName, schemas, req, hostname, port }: PrintSche const links = schemas .map((d) => { const url = `${req.protocol}://${d.table_schema}.${req.hostname}:${port}/graphiql`; + return `${d.table_schema}`; }) .join(''); diff --git a/graphql/explorer/src/resolvers/uploads.ts b/graphql/explorer/src/resolvers/uploads.ts index 637cb18f5..fd38666ff 100644 --- a/graphql/explorer/src/resolvers/uploads.ts +++ b/graphql/explorer/src/resolvers/uploads.ts @@ -52,7 +52,7 @@ export class UploadHandler { const rand = Math.random().toString(36).substring(2, 7) + Math.random().toString(36).substring(2, 7); - const key = rand + '-' + uploadNames(filename); + const key = `${rand }-${ uploadNames(filename)}`; const result = await this.streamer.upload({ readStream, diff --git a/graphql/explorer/src/server.ts b/graphql/explorer/src/server.ts index 47bc2a4d8..a7508e53a 100644 --- a/graphql/explorer/src/server.ts +++ b/graphql/explorer/src/server.ts @@ -1,7 +1,7 @@ import { getEnvOptions } from '@constructive-io/graphql-env'; +import { middleware as parseDomains } from '@constructive-io/url-domains'; import { cors, healthz, poweredBy } from '@pgpmjs/server-utils'; import { PgpmOptions } from '@pgpmjs/types'; -import { middleware as parseDomains } from '@constructive-io/url-domains'; import express, { Express, NextFunction, Request, Response } from 'express'; import { GraphileCache, graphileCache } from 'graphile-cache'; import graphqlUpload from 'graphql-upload'; @@ -53,6 +53,7 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { }; graphileCache.set(key, obj); + return obj; }; @@ -67,6 +68,7 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { app.use(async (req: Request, res: Response, next: NextFunction) => { if (req.urlDomains?.subdomains.length === 1) { const [dbName] = req.urlDomains.subdomains; + try { const pgPool = getPgPool( getPgEnvOptions({ @@ -80,6 +82,7 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { FROM pg_catalog.pg_namespace s WHERE s.nspname !~ '^pg_' AND s.nspname NOT IN ('information_schema'); `); + res.send( printSchemas({ dbName, @@ -89,23 +92,28 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { port: server.port, }) ); + return; } catch (e: any) { if (e.message?.match(/does not exist/)) { res.status(404).send('DB Not found'); + return; } console.error(e); res.status(500).send('Something happened...'); + return; } } + return next(); }); app.use(async (req: Request, res: Response, next: NextFunction) => { if (req.urlDomains?.subdomains.length === 2) { const [, dbName] = req.urlDomains.subdomains; + try { const pgPool = getPgPool( getPgEnvOptions({ @@ -118,28 +126,36 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { } catch (e: any) { if (e.message?.match(/does not exist/)) { res.status(404).send('DB Not found'); + return; } console.error(e); res.status(500).send('Something happened...'); + return; } } + return next(); }); app.use(async (req: Request, res: Response, next: NextFunction) => { if (req.urlDomains?.subdomains.length === 2) { const [schemaName, dbName] = req.urlDomains.subdomains; + try { const { handler } = getGraphileInstanceObj(dbName, schemaName); + handler(req, res, next); + return; } catch (e: any) { res.status(500).send(e.message); + return; } } + return next(); }); @@ -147,10 +163,13 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { if (req.urlDomains?.subdomains.length === 2 && req.url === '/flush') { const [schemaName, dbName] = req.urlDomains.subdomains; const key = `${dbName}.${schemaName}`; + graphileCache.delete(key); res.status(200).send('OK'); + return; } + return next(); }); @@ -168,20 +187,25 @@ export const GraphQLExplorer = (rawOpts: PgpmOptions = {}): Express => { SELECT * FROM pg_catalog.pg_database WHERE datistemplate = FALSE AND datname != 'postgres' AND datname !~ '^pg_' `); + res.send( printDatabases({ databases: results.rows, req, port: server.port }) ); + return; } catch (e: any) { if (e.message?.match(/does not exist/)) { res.status(404).send('DB Not found'); + return; } console.error(e); res.status(500).send('Something happened...'); + return; } } + return next(); }); diff --git a/graphql/explorer/src/settings.ts b/graphql/explorer/src/settings.ts index bb3cbbef8..1da2b6964 100644 --- a/graphql/explorer/src/settings.ts +++ b/graphql/explorer/src/settings.ts @@ -1,5 +1,5 @@ -import { ConstructiveOptions } from '@constructive-io/graphql-types'; import { getEnvOptions } from '@constructive-io/graphql-env'; +import { ConstructiveOptions } from '@constructive-io/graphql-types'; import { getGraphileSettings as getSettings } from 'graphile-settings'; import { PostGraphileOptions } from 'postgraphile'; diff --git a/graphql/query/__tests__/builder.test.ts b/graphql/query/__tests__/builder.test.ts index 0e30aae52..132a6077c 100644 --- a/graphql/query/__tests__/builder.test.ts +++ b/graphql/query/__tests__/builder.test.ts @@ -231,6 +231,7 @@ it('getMany edges', () => { } }) .print(); + expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); }); @@ -267,6 +268,7 @@ it('getOne', () => { } }) .print(); + expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); }); @@ -304,6 +306,7 @@ it('getAll', () => { } }) .print(); + expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); }); @@ -347,6 +350,7 @@ it('update with default scalar selection', () => { introspection }); const result = builder.query('Action').update().print(); + expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); }); @@ -367,6 +371,7 @@ it('update with custom selection', () => { } }) .print(); + expect(/(id)|(name)|(photo)|(title)/gm.test(result._hash)).toBe(true); expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); @@ -378,6 +383,7 @@ it('delete', () => { introspection }); const result = builder.query('Action').delete().print(); + expect(result._hash).toMatchSnapshot(); expect(result._queryName).toMatchSnapshot(); }); diff --git a/graphql/query/__tests__/meta-object.test.ts b/graphql/query/__tests__/meta-object.test.ts index acc438d59..8398a0709 100644 --- a/graphql/query/__tests__/meta-object.test.ts +++ b/graphql/query/__tests__/meta-object.test.ts @@ -5,6 +5,7 @@ describe('convertFromMetaSchema()', () => { it('should convert from meta schema to meta object format', () => { const metaObj = convertFromMetaSchema(apiMetaSchema); const validate = validateMetaObject(metaObj); + expect(validate).toBe(true); }); diff --git a/graphql/query/src/ast.ts b/graphql/query/src/ast.ts index a29684bba..93b0f6989 100644 --- a/graphql/query/src/ast.ts +++ b/graphql/query/src/ast.ts @@ -296,11 +296,13 @@ export const getOne = ({ isArrayNotNull } = field; let type = t.namedType({ type: fieldType }); + if (isNotNull) type = t.nonNullType({ type }); if (isArray) { type = t.listType({ type }); if (isArrayNotNull) type = t.nonNullType({ type }); } + return t.variableDefinition({ variable: t.variable({ name: fieldName }), type @@ -338,6 +340,7 @@ export const getOne = ({ }) ] }); + return ast; }; @@ -348,7 +351,8 @@ export const createOne = ({ selection }) => { if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for' + mutationName); + console.log(`no input field for mutation for${ mutationName}`); + return; } @@ -413,7 +417,8 @@ export const patchOne = ({ selection }) => { if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for' + mutationName); + console.log(`no input field for mutation for${ mutationName}`); + return; } @@ -487,7 +492,8 @@ export const patchOne = ({ export const deleteOne = ({ mutationName, operationName, mutation }) => { if (!mutation.properties?.input?.properties) { - console.log('no input field for mutation for' + mutationName); + console.log(`no input field for mutation for${ mutationName}`); + return; } @@ -506,12 +512,14 @@ export const deleteOne = ({ mutationName, operationName, mutation }) => { isArrayNotNull } = field; let type = t.namedType({ type: fieldType }); + if (isNotNull) type = t.nonNullType({ type }); if (isArray) { type = t.listType({ type }); // no need to check isArrayNotNull since we need this field for deletion type = t.nonNullType({ type }); } + return t.variableDefinition({ variable: t.variable({ name: fieldName }), type @@ -560,6 +568,7 @@ export function getSelections(selection = []) { .map((selectionDefn) => { if (selectionDefn.isObject) { const { name, selection, variables = {}, isBelongTo } = selectionDefn; + return t.field({ name, args: Object.entries(variables).reduce((args, variable) => { @@ -568,7 +577,9 @@ export function getSelections(selection = []) { name: argName, value: getValueAst(argValue) }); + args = argAst ? [...args, argAst] : args; + return args; }, []), selectionSet: isBelongTo @@ -589,13 +600,14 @@ export function getSelections(selection = []) { ] }) }); - } else { + } const { fieldDefn } = selectionDefn; + // Field is not found in model meta, do nothing if (!fieldDefn) return null; return getCustomAst(fieldDefn); - } + }) .filter(Boolean); } @@ -630,6 +642,7 @@ function getValueAst(value) { return t.objectValue({ fields: Object.entries(value).reduce((fields, entry) => { const [objKey, objValue] = entry; + fields = [ ...fields, t.objectField({ @@ -637,6 +650,7 @@ function getValueAst(value) { value: getValueAst(objValue) }) ]; + return fields; }, []) }); @@ -676,6 +690,7 @@ function getCreateVariablesAst(attrs) { type = t.listType({ type }); if (isArrayNotNull) type = t.nonNullType({ type }); } + return t.variableDefinition({ variable: t.variable({ name: fieldName }), type @@ -709,6 +724,7 @@ function getUpdateVariablesAst(attrs, patchers) { if (isNotNull) type = t.nonNullType({ type }); if (isArray) type = t.listType({ type }); if (patchers.includes(field.name)) type = t.nonNullType({ type }); + return t.variableDefinition({ variable: t.variable({ name: fieldName }), type diff --git a/graphql/query/src/custom-ast.ts b/graphql/query/src/custom-ast.ts index 4e4b0f725..ec05bb8b3 100644 --- a/graphql/query/src/custom-ast.ts +++ b/graphql/query/src/custom-ast.ts @@ -4,6 +4,7 @@ import * as t from 'gql-ast'; export function getCustomAst(fieldDefn) { const { pgType } = fieldDefn.type; + if (pgType === 'geometry') { return geometryAst(fieldDefn.name); } diff --git a/graphql/query/src/index.ts b/graphql/query/src/index.ts index bbae82d24..15b3dbe12 100644 --- a/graphql/query/src/index.ts +++ b/graphql/query/src/index.ts @@ -18,6 +18,7 @@ export class QueryBuilder { this.pickAllFields = pickAllFields.bind(this); const result = validateMetaObject(this._meta); + if (typeof result === 'object' && result.errors) { throw new Error(`QueryBuilder: meta object is invalid:\n${result.message}`); } @@ -29,6 +30,7 @@ export class QueryBuilder { initModelMap() { this._models = Object.keys(this._introspection).reduce((map, key) => { const defn = this._introspection[key]; + map = { ...map, [defn.model]: { @@ -36,6 +38,7 @@ export class QueryBuilder { ...{ [key]: defn } } }; + return map; }, {}); } @@ -52,14 +55,16 @@ export class QueryBuilder { query(model) { this.clear(); this._model = model; + return this; } _findQuery() { // based on the op, finds the relevant GQL query const queries = this._models[this._model]; + if (!queries) { - throw new Error('No queries found for ' + this._model); + throw new Error(`No queries found for ${ this._model}`); } const matchQuery = Object.entries(queries).find( @@ -67,10 +72,11 @@ export class QueryBuilder { ); if (!matchQuery) { - throw new Error('No query found for ' + this._model + ':' + this._op); + throw new Error(`No query found for ${ this._model }:${ this._op}`); } const queryKey = matchQuery[0]; + return queryKey; } @@ -80,6 +86,7 @@ export class QueryBuilder { const matchingDefns = Object.keys(this._introspection).reduce( (arr, mutationKey) => { const defn = this._introspection[mutationKey]; + if ( defn.model === this._model && defn.qtype === this._op && @@ -88,6 +95,7 @@ export class QueryBuilder { ) { arr = [...arr, { defn, mutationKey }]; } + return arr; }, [] @@ -95,7 +103,7 @@ export class QueryBuilder { if (!matchingDefns.length === 0) { throw new Error( - 'no mutation found for ' + this._model + ':' + this._mutation + `no mutation found for ${ this._model }:${ this._mutation}` ); } @@ -112,7 +120,7 @@ export class QueryBuilder { return `Update${inflection.camelize(this._model)}Input`; } default: - throw new Error('Unhandled mutation type' + mutationType); + throw new Error(`Unhandled mutation type${ mutationType}`); } }; @@ -122,7 +130,7 @@ export class QueryBuilder { if (!matchDefn) { throw new Error( - 'no mutation found for ' + this._model + ':' + this._mutation + `no mutation found for ${ this._model }:${ this._mutation}` ); } @@ -135,15 +143,18 @@ export class QueryBuilder { // If selection not given, pick only scalar fields if (selection == null) { this._select = this.pickScalarFields(null, defn); + return this; } this._select = this.pickAllFields(selection, defn); + return this; } edges(useEdges) { this._edges = useEdges; + return this; } @@ -209,6 +220,7 @@ export class QueryBuilder { ); const defn = this._introspection[this._key]; + this.select(select); this._ast = getOne({ builder: this, @@ -234,6 +246,7 @@ export class QueryBuilder { ); const defn = this._introspection[this._key]; + this.select(select); this._ast = createOne({ builder: this, @@ -300,11 +313,13 @@ export class QueryBuilder { queryName(name) { this._queryName = name; + return this; } print() { this._hash = gqlPrint(this._ast); + return this; } } @@ -327,6 +342,7 @@ function pickScalarFields(selection, defn) { .filter((fieldName) => { // If not specified or not a valid selection list, allow all if (selection == null || !Array.isArray(selection)) return true; + return selection.includes(fieldName); }) .filter( @@ -345,6 +361,7 @@ function pickScalarFields(selection, defn) { const relatedQuery = this._introspection[ `${modelNameToGetMany(defn.model)}` ]; + return pickFrom(relatedQuery.selection); } @@ -370,6 +387,7 @@ function pickAllFields(selection, defn) { for (const entry of selectionEntries) { const [fieldName, fieldOptions] = entry; + // Case // { // goalResults: // fieldName @@ -412,6 +430,7 @@ function pickAllFields(selection, defn) { referencedForeignConstraint.refTable ); const refDefn = this._introspection[getManyName]; + fieldSelection.selection = pickScalarFields.call( this, subFields, @@ -457,6 +476,7 @@ function isFieldInDefinition(fieldName, defn, modelMeta) { if (isObject(selectionItem)) { return selectionItem.name === fieldName; } + return false; }) ); diff --git a/graphql/query/src/meta-object/convert.ts b/graphql/query/src/meta-object/convert.ts index ca3931e1a..444d2d846 100644 --- a/graphql/query/src/meta-object/convert.ts +++ b/graphql/query/src/meta-object/convert.ts @@ -28,6 +28,7 @@ export function convertFromMetaSchema(metaSchema) { function pickArrayConstraint(constraints) { if (constraints.length === 0) return []; const c = constraints[0]; + return c.fields.map((field) => pickField(field)); } @@ -44,6 +45,7 @@ function pickForeignConstraint(constraints, relations) { const matchingBelongsTo = belongsTo.find((c) => { const field = pickField(c.keys[0]); + return field.name === fromKey.name; }); diff --git a/graphql/react/__tests__/basic.test.tsx b/graphql/react/__tests__/basic.test.tsx index d0090efc6..a8da02423 100644 --- a/graphql/react/__tests__/basic.test.tsx +++ b/graphql/react/__tests__/basic.test.tsx @@ -1,18 +1,21 @@ // @ts-nocheck jest.setTimeout(20000); -import React from 'react'; -import { render, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; + +import { cleanup,render } from '@testing-library/react'; +import React from 'react'; +import { QueryClient,QueryClientProvider } from 'react-query'; +import { useQuery } from 'react-query'; + import { LqlProvider, - useGraphqlClient, useConstructiveQuery, + useGraphqlClient, useTableRowsPaginated } from '../src'; -import { QueryClientProvider, QueryClient } from 'react-query'; -import { useQuery } from 'react-query'; const TESTING_URL = process.env.TESTING_URL; + if (!TESTING_URL) { throw new Error( 'process.env.TESTING_URL required. Please set a GraphQL Endpoint.' @@ -24,6 +27,7 @@ afterEach(cleanup); it('works', async () => { const { container } = render(

Hello, World!

); + expect(container.firstChild).toMatchInlineSnapshot(`

Hello, World! @@ -34,6 +38,7 @@ it('works', async () => { const Component = () => { const graphqlClient = useGraphqlClient(); const queryBuilder = useConstructiveQuery(); + return
Yolo
; }; @@ -56,6 +61,7 @@ const FetchUsers = ({ onSuccess, onError }) => { onError // ...queryParams }); + if (isLoading || isFetching) { return
Loading
; } @@ -64,6 +70,7 @@ const FetchUsers = ({ onSuccess, onError }) => { expect(error).toBeFalsy(); expect(data.nodes).toBeTruthy(); } + return
Finished
; }; @@ -77,6 +84,7 @@ const BasicQuery = ({ onSuccess, onError }) => { currentUserAgent } `); + return result; }, { @@ -85,6 +93,7 @@ const BasicQuery = ({ onSuccess, onError }) => { enabled: !!graphqlClient } ); + if (isLoading || isFetching) { return
Loading
; } @@ -93,6 +102,7 @@ const BasicQuery = ({ onSuccess, onError }) => { expect(error).toBeFalsy(); expect(data.nodes).toBeTruthy(); } + return
Finished
; }; @@ -124,6 +134,7 @@ it('useTableRowsPaginated', async (done) => { expect(true).toBe(false); done(); }; + render( @@ -143,6 +154,7 @@ it('useGraphqlClient', async (done) => { expect(true).toBe(false); done(); }; + render( diff --git a/graphql/react/src/context.ts b/graphql/react/src/context.ts index c235d24b5..c2db007c8 100644 --- a/graphql/react/src/context.ts +++ b/graphql/react/src/context.ts @@ -8,11 +8,13 @@ const cache = new (useReactNative ? WeakMap : Map)(); export function getLqlContext() { let context = cache.get(React.createContext); + if (!context) { context = React.createContext({}); context.displayName = 'LqlContext'; cache.set(React.createContext, context); } + return context; } diff --git a/graphql/react/src/index.ts b/graphql/react/src/index.ts index 6f9f53c06..87b02526a 100644 --- a/graphql/react/src/index.ts +++ b/graphql/react/src/index.ts @@ -1,7 +1,7 @@ -export * from './use-graphql-client'; +export * from './context'; +export * from './provider'; export * from './use-constructive-client'; +export * from './use-graphql-client'; export * from './use-introspection'; export * from './use-schema-meta'; export * from './use-table-rows'; -export * from './provider'; -export * from './context'; diff --git a/graphql/react/src/provider.tsx b/graphql/react/src/provider.tsx index c78d51090..8234233d0 100644 --- a/graphql/react/src/provider.tsx +++ b/graphql/react/src/provider.tsx @@ -1,9 +1,11 @@ // @ts-nocheck import React from 'react'; + import { getLqlContext } from './context'; export const LqlProvider = ({ endpointUrl, children }) => { const LqlContext = getLqlContext(); + return ( {(context = {}) => { @@ -12,7 +14,7 @@ export const LqlProvider = ({ endpointUrl, children }) => { } if (!context.endpointUrl) { throw new Error( - 'LqlProvider was not passed endpointUrl: ' + JSON.stringify(context) + `LqlProvider was not passed endpointUrl: ${ JSON.stringify(context)}` ); } diff --git a/graphql/react/src/use-constructive-client.ts b/graphql/react/src/use-constructive-client.ts index 6f6451b48..c635e904d 100644 --- a/graphql/react/src/use-constructive-client.ts +++ b/graphql/react/src/use-constructive-client.ts @@ -1,5 +1,6 @@ +import { MetaObject,QueryBuilder } from '@constructive-io/graphql-query'; import { useMemo } from 'react'; -import { QueryBuilder, MetaObject } from '@constructive-io/graphql-query'; + import { useIntrospection } from './use-introspection'; import { useSchemaMeta } from './use-schema-meta'; @@ -9,6 +10,7 @@ export function useConstructiveQuery() { return useMemo(() => { if (!meta.data || !introspection.data) return null; + return new QueryBuilder({ meta: MetaObject.convertFromMetaSchema({ _meta: meta.data }), introspection: introspection.data diff --git a/graphql/react/src/use-graphql-client.ts b/graphql/react/src/use-graphql-client.ts index a1abe5d98..26b6ed679 100644 --- a/graphql/react/src/use-graphql-client.ts +++ b/graphql/react/src/use-graphql-client.ts @@ -1,6 +1,7 @@ // @ts-nocheck -import { useState, useMemo, useContext } from 'react'; import { GraphQLClient } from 'graphql-request'; +import { useContext,useMemo, useState } from 'react'; + import { getLqlContext } from './context'; export const useGraphqlClient = () => { @@ -8,6 +9,7 @@ export const useGraphqlClient = () => { const [headers, setHeaders] = useState({}); const context = useContext(getLqlContext()); + if (!context || !context.endpointUrl) { throw new Error('Missing LqlProvider'); } diff --git a/graphql/react/src/use-introspection.ts b/graphql/react/src/use-introspection.ts index 97df5d7d5..590c27bb9 100644 --- a/graphql/react/src/use-introspection.ts +++ b/graphql/react/src/use-introspection.ts @@ -1,7 +1,8 @@ -import { useEffect } from 'react'; -import { useQuery } from 'react-query'; // @ts-ignore import { IntrospectionQuery, parseGraphQuery } from 'introspectron'; +import { useEffect } from 'react'; +import { useQuery } from 'react-query'; + import { useGraphqlClient } from './use-graphql-client'; const noop = () => {}; @@ -23,8 +24,10 @@ export function useIntrospection(options = {}) { const introspectionResults = await graphqlClient.request( IntrospectionQuery ); + try { const { queries, mutations } = parseGraphQuery(introspectionResults); + return { ...queries, ...mutations }; } catch (err) { throw new Error('useIntrospection: failed to get introspection query'); diff --git a/graphql/react/src/use-schema-meta.ts b/graphql/react/src/use-schema-meta.ts index 8d6b1fdf6..937ca1ac8 100644 --- a/graphql/react/src/use-schema-meta.ts +++ b/graphql/react/src/use-schema-meta.ts @@ -1,7 +1,8 @@ // @ts-nocheck -import { useEffect } from 'react'; import { gql } from 'graphql-request'; +import { useEffect } from 'react'; import { useQuery } from 'react-query'; + import { useGraphqlClient } from './use-graphql-client'; const fieldFragment = ` diff --git a/graphql/react/src/use-table-rows.ts b/graphql/react/src/use-table-rows.ts index ef173bf00..771c2535d 100644 --- a/graphql/react/src/use-table-rows.ts +++ b/graphql/react/src/use-table-rows.ts @@ -1,13 +1,14 @@ // @ts-nocheck import { useMemo } from 'react'; import { - useQuery, useInfiniteQuery, useMutation, + useQuery, useQueryClient } from 'react-query'; -import { useGraphqlClient } from './use-graphql-client'; + import { useConstructiveQuery } from './use-constructive-client'; +import { useGraphqlClient } from './use-graphql-client'; const noop = () => {}; export function useTableRowsPaginated(options = {}) { @@ -33,6 +34,7 @@ export function useTableRowsPaginated(options = {}) { const graphqlQuery = useMemo(() => { if (!queryBuilder) return null; const result = queryBuilder.query(tableName).getMany({ select }).print(); + return { hash: result._hash, key: result._key @@ -69,6 +71,7 @@ export function useTableRowsPaginated(options = {}) { orderBy, graphqlQuery } = normalize(params); + if (skip || !graphqlQuery) return null; const result = await graphqlClient.request(graphqlQuery.hash, { @@ -80,6 +83,7 @@ export function useTableRowsPaginated(options = {}) { filter, orderBy }); + return result[graphqlQuery.key]; }, { @@ -114,6 +118,7 @@ export function useTableRowsInfinite(options = {}) { const graphqlQuery = useMemo(() => { if (!queryBuilder) return null; const result = queryBuilder.query(tableName).getMany({ select }).print(); + return { hash: result._hash, key: result._key @@ -135,6 +140,7 @@ export function useTableRowsInfinite(options = {}) { async ({ queryKey, pageParam = null }) => { const [, params] = queryKey; const { condition, filter, orderBy, graphqlQuery } = normalize(params); + if (!graphqlQuery) return null; const result = await graphqlClient.request(graphqlQuery.hash, { @@ -144,6 +150,7 @@ export function useTableRowsInfinite(options = {}) { filter, orderBy }); + return result[graphqlQuery.key]; }, { @@ -168,6 +175,7 @@ export function useCreateTableRow(options = {}) { const graphqlMutation = useMemo(() => { if (!queryBuilder) return null; const result = queryBuilder.query(tableName).create().print(); + return { hash: result._hash, key: result._key @@ -180,6 +188,7 @@ export function useCreateTableRow(options = {}) { graphqlMutation.hash, variables ); + return result[graphqlMutation.key]; }, { @@ -206,6 +215,7 @@ export function useDeleteTableRow(options = {}) { const graphqlMutation = useMemo(() => { if (!queryBuilder) return null; const result = queryBuilder.query(tableName).delete().print(); + return { hash: result._hash, key: result._key @@ -218,6 +228,7 @@ export function useDeleteTableRow(options = {}) { graphqlMutation.hash, variables ); + return result[graphqlMutation.key]; }, { diff --git a/graphql/server/__tests__/schema.test.ts b/graphql/server/__tests__/schema.test.ts index d8d09f0ea..5075cfe2d 100644 --- a/graphql/server/__tests__/schema.test.ts +++ b/graphql/server/__tests__/schema.test.ts @@ -1,14 +1,14 @@ process.env.LOG_SCOPE = 'graphile-test'; -import { join } from 'path'; import express from 'express'; -import { postgraphile } from 'postgraphile'; // @ts-ignore import { getGraphileSettings } from 'graphile-settings'; +import { getConnections,seed } from 'graphile-test'; +import { join } from 'path'; import { getPgPool } from 'pg-cache'; +import { postgraphile } from 'postgraphile'; import { buildSchemaSDL, fetchEndpointSchemaSDL } from '../src/schema'; -import { seed, getConnections } from 'graphile-test'; jest.setTimeout(30000); @@ -29,6 +29,7 @@ beforeAll(async () => { ]) ] ); + ({ db, teardown } = connections); }); @@ -53,6 +54,7 @@ it('buildSchemaSDL returns a valid GraphQL SDL string', async () => { database: db.config.database, schemas }); + expect(typeof sdl).toBe('string'); expect(sdl.length).toBeGreaterThan(100); expect(sdl).toContain('type Query'); diff --git a/graphql/server/src/index.ts b/graphql/server/src/index.ts index 466859838..d7dbc63cf 100644 --- a/graphql/server/src/index.ts +++ b/graphql/server/src/index.ts @@ -1,2 +1,2 @@ -export * from './server'; -export * from './schema'; \ No newline at end of file +export * from './schema'; +export * from './server'; \ No newline at end of file diff --git a/graphql/server/src/middleware/api.ts b/graphql/server/src/middleware/api.ts index 37648d153..d253dea9e 100644 --- a/graphql/server/src/middleware/api.ts +++ b/graphql/server/src/middleware/api.ts @@ -1,3 +1,5 @@ +import './types'; // for Request type + import { getNodeEnv } from '@constructive-io/graphql-env'; import { svcCache } from '@pgpmjs/server-utils'; import { PgpmOptions } from '@pgpmjs/types'; @@ -14,7 +16,6 @@ import errorPage404Message from '../errors/404-message'; */ import { ApiStructure, Domain, SchemaNode, Service, Site } from '../types'; import { ApiByNameQuery, ApiQuery, ListOfAllDomainsOfDb } from './gql'; -import './types'; // for Request type const transformServiceToApi = (svc: Service): ApiStructure => { const api = svc.data.api; @@ -24,6 +25,7 @@ const transformServiceToApi = (svc: Service): ApiStructure => { api.schemaNames?.nodes?.map((n: SchemaNode) => n.schemaName) || []; let domains: string[] = []; + if (api.database?.sites?.nodes) { domains = api.database.sites.nodes.reduce((acc: string[], site: Site) => { if (site.domains?.nodes && site.domains.nodes.length) { @@ -33,10 +35,13 @@ const transformServiceToApi = (svc: Service): ApiStructure => { : domain.domain; const protocol = domain.domain === 'localhost' ? 'http://' : 'https://'; + return protocol + hostname; }); + return [...acc, ...siteUrls]; } + return acc; }, []); } @@ -60,14 +65,17 @@ const transformServiceToApi = (svc: Service): ApiStructure => { const getPortFromRequest = (req: Request): string | null => { const host = req.headers.host; + if (!host) return null; const parts = host.split(':'); + return parts.length === 2 ? `:${parts[1]}` : null; }; export const getSubdomain = (reqDomains: string[]): string | null => { const names = reqDomains.filter((name) => !['www'].includes(name)); + return !names.length ? null : names.join('.'); }; @@ -92,8 +100,10 @@ export const createApiMiddleware = (opts: any) => { databaseId, isPublic: false, }; + req.api = api; req.databaseId = databaseId; + return next(); } try { @@ -103,8 +113,9 @@ export const createApiMiddleware = (opts: any) => { res .status(404) .send(errorPage404Message('API not found', svc.errorHtml)); + return; - } else if (!svc) { + } if (!svc) { res .status(404) .send( @@ -112,9 +123,11 @@ export const createApiMiddleware = (opts: any) => { 'API service not found for the given domain/subdomain.' ) ); + return; } const api = transformServiceToApi(svc); + req.api = api; req.databaseId = api.databaseId; next(); @@ -167,7 +180,9 @@ const getHardCodedSchemata = ({ }, }, }; + svcCache.set(key, svc); + return svc; }; @@ -198,7 +213,9 @@ const getMetaSchema = ({ }, }, }; + svcCache.set(key, svc); + return svc; }; @@ -223,16 +240,21 @@ const queryServiceByDomainAndSubdomain = async ({ if (result.errors?.length) { console.error(result.errors); + return null; } const nodes = result?.data?.domains?.nodes; + if (nodes?.length) { const data = nodes[0]; const apiPublic = (opts as any).api?.isPublic; + if (!data.api || data.api.isPublic !== apiPublic) return null; const svc = { data }; + svcCache.set(key, svc); + return svc; } @@ -260,16 +282,21 @@ const queryServiceByApiName = async ({ if (result.errors?.length) { console.error(result.errors); + return null; } const data = result?.data; const apiPublic = (opts as any).api?.isPublic; + if (data?.api && data.api.isPublic === apiPublic) { const svc = { data }; + svcCache.set(key, svc); + return svc; } + return null; }; @@ -281,19 +308,21 @@ const getSvcKey = (opts: PgpmOptions, req: Request): string => { .join('.'); const apiPublic = (opts as any).api?.isPublic; + if (apiPublic === false) { if (req.get('X-Api-Name')) { - return 'api:' + req.get('X-Database-Id') + ':' + req.get('X-Api-Name'); + return `api:${ req.get('X-Database-Id') }:${ req.get('X-Api-Name')}`; } if (req.get('X-Schemata')) { return ( - 'schemata:' + req.get('X-Database-Id') + ':' + req.get('X-Schemata') + `schemata:${ req.get('X-Database-Id') }:${ req.get('X-Schemata')}` ); } if (req.get('X-Meta-Schema')) { - return 'metaschema:api:' + req.get('X-Database-Id'); + return `metaschema:api:${ req.get('X-Database-Id')}`; } } + return key; }; @@ -305,6 +334,7 @@ const validateSchemata = async ( `SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemata] ); + return result.rows.map((row: { schema_name: string }) => row.schema_name); }; @@ -318,9 +348,11 @@ export const getApiConfig = async ( const domain: string = req.urlDomains.domain as string; const key = getSvcKey(opts, req); + req.svc_key = key; let svc; + if (svcCache.has(key)) { svc = svcCache.get(key); } else { @@ -331,6 +363,7 @@ export const getApiConfig = async ( if (validatedSchemata.length === 0) { const message = `No valid schemas found for domain: ${domain}, subdomain: ${subdomain}`; const error: any = new Error(message); + error.code = 'NO_VALID_SCHEMAS'; throw error; } @@ -347,6 +380,7 @@ export const getApiConfig = async ( const client = new GraphileQuery({ schema, pool: rootPgPool, settings }); const apiPublic = (opts as any).api?.isPublic; + if (apiPublic === false) { if (req.get('X-Schemata')) { svc = getHardCodedSchemata({ @@ -415,14 +449,14 @@ export const getApiConfig = async ( ); const linksHtml = allDomains.length - ? `
    ` + + ? `
      ${ allDomains .map( (d: any) => `
    • ${d.href}
    • ` ) - .join('') + - `
    ` + .join('') + }
` : `

No APIs are currently registered for this database.

`; const errorHtml = ` @@ -440,5 +474,6 @@ export const getApiConfig = async ( } } } + return svc; }; diff --git a/graphql/server/src/middleware/auth.ts b/graphql/server/src/middleware/auth.ts index f5a907b1d..a39a29919 100644 --- a/graphql/server/src/middleware/auth.ts +++ b/graphql/server/src/middleware/auth.ts @@ -1,8 +1,9 @@ +import './types'; // for Request type + import { PgpmOptions } from '@pgpmjs/types'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { getPgPool } from 'pg-cache'; import pgQueryContext from 'pg-query-context'; -import './types'; // for Request type export const createAuthenticateMiddleware = ( opts: PgpmOptions @@ -13,8 +14,10 @@ export const createAuthenticateMiddleware = ( next: NextFunction ): Promise => { const api = req.api; + if (!api) { res.status(500).send('Missing API info'); + return; } @@ -59,6 +62,7 @@ export const createAuthenticateMiddleware = ( res.status(200).json({ errors: [{ extensions: { code: 'UNAUTHENTICATED' } }], }); + return; } @@ -74,6 +78,7 @@ export const createAuthenticateMiddleware = ( }, ], }); + return; } } diff --git a/graphql/server/src/middleware/cors.ts b/graphql/server/src/middleware/cors.ts index f7980f72f..d4af466bf 100644 --- a/graphql/server/src/middleware/cors.ts +++ b/graphql/server/src/middleware/cors.ts @@ -1,8 +1,10 @@ +import './types'; // for Request type + import { parseUrl } from '@constructive-io/url-domains'; import corsPlugin from 'cors'; import type { Request, RequestHandler } from 'express'; + import { CorsModuleData } from '../types'; -import './types'; // for Request type /** * Unified CORS middleware for Constructive API @@ -34,6 +36,7 @@ export const cors = (fallbackOrigin?: string): RequestHandler => { // 2) Per-API allowlist sourced from req.api (if available) // createApiMiddleware runs before this in server.ts, so req.api should be set const api = (req as any).api as { apiModules?: any[]; domains?: string[] } | undefined; + if (api) { const corsModules = (api.apiModules || []).filter((m: any) => m.name === 'cors') as { name: 'cors'; data: CorsModuleData }[]; const siteUrls = api.domains || []; @@ -48,6 +51,7 @@ export const cors = (fallbackOrigin?: string): RequestHandler => { if (origin) { try { const parsed = parseUrl(new URL(origin)); + if (parsed.domain === 'localhost') { return callback(null, true); } diff --git a/graphql/server/src/middleware/flush.ts b/graphql/server/src/middleware/flush.ts index bf7bb79c3..720b85d42 100644 --- a/graphql/server/src/middleware/flush.ts +++ b/graphql/server/src/middleware/flush.ts @@ -1,10 +1,11 @@ +import './types'; // for Request type + import { ConstructiveOptions } from '@constructive-io/graphql-types'; import { Logger } from '@pgpmjs/logger'; import { svcCache } from '@pgpmjs/server-utils'; import { NextFunction,Request, Response } from 'express'; import { graphileCache } from 'graphile-cache'; import { getPgPool } from 'pg-cache'; -import './types'; // for Request type const log = new Logger('flush'); @@ -14,14 +15,17 @@ export const flush = async (req: Request, res: Response, next: NextFunction): Pr graphileCache.delete((req as any).svc_key); svcCache.delete((req as any).svc_key); res.status(200).send('OK'); + return; } + return next(); }; export const flushService = async (opts: ConstructiveOptions, databaseId: string): Promise => { const pgPool = getPgPool(opts.pg); - log.info('flushing db ' + databaseId); + + log.info(`flushing db ${ databaseId}`); const api = new RegExp(`^api:${databaseId}:.*`); const schemata = new RegExp(`^schemata:${databaseId}:.*`); @@ -47,6 +51,7 @@ export const flushService = async (opts: ConstructiveOptions, databaseId: string for (const row of svc.rows) { let key: string | undefined; + if (row.domain && !row.subdomain) { key = row.domain; } else if (row.domain && row.subdomain) { diff --git a/graphql/server/src/middleware/graphile.ts b/graphql/server/src/middleware/graphile.ts index 5ac769791..3be90074b 100644 --- a/graphql/server/src/middleware/graphile.ts +++ b/graphql/server/src/middleware/graphile.ts @@ -1,3 +1,5 @@ +import './types'; // for Request type + import { ConstructiveOptions } from '@constructive-io/graphql-types'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { graphileCache } from 'graphile-cache'; @@ -5,7 +7,6 @@ import { getGraphileSettings as getSettings } from 'graphile-settings'; import type { IncomingMessage } from 'http'; import { getPgPool } from 'pg-cache'; import { postgraphile, PostGraphileOptions } from 'postgraphile'; -import './types'; // for Request type import PublicKeySignature, { PublicKeyChallengeConfig, @@ -15,10 +16,12 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { return async (req: Request, res: Response, next: NextFunction) => { try { const api = req.api; + if (!api) { return res.status(500).send('Missing API info'); } const key = req.svc_key; + if (!key) { return res.status(500).send('Missing service cache key'); } @@ -26,6 +29,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { if (graphileCache.has(key)) { const { handler } = graphileCache.get(key)!; + return handler(req, res, next); } @@ -33,7 +37,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { ...opts, graphile: { ...opts.graphile, - schema: schema, + schema, }, }); diff --git a/graphql/server/src/middleware/types.ts b/graphql/server/src/middleware/types.ts index 6864e20d8..659322393 100644 --- a/graphql/server/src/middleware/types.ts +++ b/graphql/server/src/middleware/types.ts @@ -1,4 +1,3 @@ -import type { Request } from 'express'; import { ApiModule } from "../types"; diff --git a/graphql/server/src/schema.ts b/graphql/server/src/schema.ts index 1f02f6067..7528af75f 100644 --- a/graphql/server/src/schema.ts +++ b/graphql/server/src/schema.ts @@ -1,9 +1,10 @@ -import { printSchema, GraphQLSchema, getIntrospectionQuery, buildClientSchema } from 'graphql' +import * as http from 'node:http' +import * as https from 'node:https' + import { getGraphileSettings } from 'graphile-settings' +import { buildClientSchema,getIntrospectionQuery, GraphQLSchema, printSchema } from 'graphql' import { getPgPool } from 'pg-cache' import { createPostGraphileSchema, PostGraphileOptions } from 'postgraphile' -import * as http from 'node:http' -import * as https from 'node:https' export type BuildSchemaOptions = { database?: string; @@ -25,6 +26,7 @@ export async function buildSchemaSDL(opts: BuildSchemaOptions): Promise const pgPool = getPgPool({ database }) const schema: GraphQLSchema = await createPostGraphileSchema(pgPool, schemas, settings) + return printSchema(schema) } @@ -45,6 +47,7 @@ export async function fetchEndpointSchemaSDL(endpoint: string, opts?: { headerHo 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(postData)), } + if (opts?.headerHost) { headers['Host'] = opts.headerHost } @@ -69,21 +72,25 @@ export async function fetchEndpointSchemaSDL(endpoint: string, opts?: { headerHo headers, }, (res) => { let data = '' + res.on('data', (chunk) => { data += chunk }) res.on('end', () => { if (res.statusCode && res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode} – ${data}`)) + return } resolve(data) }) }) + req.on('error', (err) => reject(err)) req.write(postData) req.end() }) let json: any + try { json = JSON.parse(responseData) } catch (e) { @@ -98,5 +105,6 @@ export async function fetchEndpointSchemaSDL(endpoint: string, opts?: { headerHo } const schema = buildClientSchema(json.data as any) + return printSchema(schema) } diff --git a/graphql/server/src/scripts/create-bucket.ts b/graphql/server/src/scripts/create-bucket.ts index 8b2272220..5c4aac6e4 100644 --- a/graphql/server/src/scripts/create-bucket.ts +++ b/graphql/server/src/scripts/create-bucket.ts @@ -2,8 +2,8 @@ // Avoid strict type coupling between different @aws-sdk/client-s3 versions import { S3Client } from '@aws-sdk/client-s3'; -import { createS3Bucket } from '@constructive-io/s3-utils'; import { getEnvOptions } from '@constructive-io/graphql-env'; +import { createS3Bucket } from '@constructive-io/s3-utils'; (async () => { try { @@ -27,6 +27,7 @@ import { getEnvOptions } from '@constructive-io/graphql-env'; process.env.IS_MINIO = 'true'; const res = await createS3Bucket(client as any, bucket); + console.log(`[create-bucket] ${bucket}:`, res); client.destroy(); diff --git a/graphql/server/src/server.ts b/graphql/server/src/server.ts index aa64a778f..4576c692c 100644 --- a/graphql/server/src/server.ts +++ b/graphql/server/src/server.ts @@ -1,8 +1,8 @@ import { getEnvOptions } from '@constructive-io/graphql-env'; +import { middleware as parseDomains } from '@constructive-io/url-domains'; import { Logger } from '@pgpmjs/logger'; import { healthz, poweredBy, trustProxy } from '@pgpmjs/server-utils'; import { PgpmOptions } from '@pgpmjs/types'; -import { middleware as parseDomains } from '@constructive-io/url-domains'; import express, { Express, RequestHandler } from 'express'; // @ts-ignore import graphqlUpload from 'graphql-upload'; @@ -21,6 +21,7 @@ const log = new Logger('server'); export const GraphQLServer = (rawOpts: PgpmOptions = {}) => { const envOptions = getEnvOptions(rawOpts); const app = new Server(envOptions); + app.addEventListener(); app.listen(); }; @@ -40,6 +41,7 @@ class Server { trustProxy(app, opts.server.trustProxy); // Warn if a global CORS override is set in production const fallbackOrigin = opts.server?.origin?.trim(); + if (fallbackOrigin && process.env.NODE_ENV === 'production') { if (fallbackOrigin === '*') { log.warn('CORS wildcard ("*") is enabled in production; this effectively disables CORS and is not recommended. Prefer per-API CORS via meta schema.'); @@ -90,6 +92,7 @@ class Server { addEventListener(): void { const pgPool = this.getPool(); + pgPool.connect(this.listenForChanges.bind(this)); } @@ -101,6 +104,7 @@ class Server { if (err) { this.error('Error connecting with notify listener', err); setTimeout(() => this.addEventListener(), 5000); + return; } diff --git a/graphql/test/__tests__/graphile-test.fail.test.ts b/graphql/test/__tests__/graphile-test.fail.test.ts index 3b24d017c..b859a93eb 100644 --- a/graphql/test/__tests__/graphile-test.fail.test.ts +++ b/graphql/test/__tests__/graphile-test.fail.test.ts @@ -86,6 +86,7 @@ it('aborts transaction when inserting duplicate usernames', async () => { // Any query run before rollback/commit will throw: // "current transaction is aborted, commands ignored until end of transaction block" const res = await pg.client.query(`select * from app_public.users`); + console.log('After failed tx:', res.rows); // Should not be reached! } catch (err) { const txErr = err as Error; diff --git a/graphql/test/__tests__/graphile-test.graphile-tx.test.ts b/graphql/test/__tests__/graphile-test.graphile-tx.test.ts index 2ce421db8..ac5d6b791 100644 --- a/graphql/test/__tests__/graphile-test.graphile-tx.test.ts +++ b/graphql/test/__tests__/graphile-test.graphile-tx.test.ts @@ -1,13 +1,13 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import gql from 'graphql-tag'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; @@ -84,6 +84,7 @@ it('handles duplicate insert via internal PostGraphile savepoint', async () => { // βœ… Step 1: Insert should succeed const first = await query(CREATE_USER, input); + expect(snapshot(first)).toMatchSnapshot('firstInsert'); expect(first.errors).toBeUndefined(); expect(first.data?.createUser?.user?.username).toBe('dupeuser'); @@ -93,12 +94,14 @@ it('handles duplicate insert via internal PostGraphile savepoint', async () => { // So this error will be caught and rolled back to that SAVEPOINT. // The transaction remains clean and usable. const second = await query(CREATE_USER, input); + expect(snapshot(second)).toMatchSnapshot('duplicateInsert'); expect(second.errors?.[0]?.message).toMatch(/duplicate key value/i); expect(second.data?.createUser).toBeNull(); // βœ… Step 3: Query still works β€” transaction was not aborted const followup = await query(GET_USERS); + expect(snapshot(followup)).toMatchSnapshot('queryAfterDuplicateInsert'); expect(followup.errors).toBeUndefined(); expect(followup.data?.users?.nodes.some((u: any) => u.username === 'dupeuser')).toBe(true); diff --git a/graphql/test/__tests__/graphile-test.graphql.test.ts b/graphql/test/__tests__/graphile-test.graphql.test.ts index 5570584b4..247615f29 100644 --- a/graphql/test/__tests__/graphile-test.graphql.test.ts +++ b/graphql/test/__tests__/graphile-test.graphql.test.ts @@ -1,13 +1,13 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import gql from 'graphql-tag'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; diff --git a/graphql/test/__tests__/graphile-test.plugins.test.ts b/graphql/test/__tests__/graphile-test.plugins.test.ts index 7318863a8..c347a1adb 100644 --- a/graphql/test/__tests__/graphile-test.plugins.test.ts +++ b/graphql/test/__tests__/graphile-test.plugins.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import gql from 'graphql-tag'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; import { IntrospectionQuery } from '../test-utils/queries'; const schemas = ['app_public']; @@ -16,6 +16,7 @@ const sql = (f: string) => join(__dirname, '/../sql', f); const TestPlugin = (builder: any) => { builder.hook('GraphQLObjectType:fields', (fields: any, build: any, context: any) => { const { scope } = context; + if (scope.isRootQuery) { return build.extend(fields, { testPluginField: { @@ -24,6 +25,7 @@ const TestPlugin = (builder: any) => { } }); } + return fields; }); }; @@ -32,6 +34,7 @@ const TestPlugin = (builder: any) => { const AnotherTestPlugin = (builder: any) => { builder.hook('GraphQLObjectType:fields', (fields: any, build: any, context: any) => { const { scope } = context; + if (scope.isRootQuery) { return build.extend(fields, { anotherTestField: { @@ -40,6 +43,7 @@ const AnotherTestPlugin = (builder: any) => { } }); } + return fields; }); }; @@ -82,22 +86,26 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); it('should include plugin field in introspection', async () => { const res = await query(IntrospectionQuery); + expect(res.data).not.toBeNull(); expect(res.data).not.toBeUndefined(); expect(res.errors).toBeUndefined(); const queryTypeName = res.data?.__schema?.queryType?.name; + expect(queryTypeName).toBe('Query'); // Find the Query type in the types array const types = res.data?.__schema?.types || []; const queryType = types.find((t: any) => t.name === queryTypeName); + expect(queryType).not.toBeNull(); expect(queryType).not.toBeUndefined(); expect(queryType?.name).toBe('Query'); @@ -105,6 +113,7 @@ describe('graphile-test with plugins', () => { const fields = queryType?.fields || []; const testField = fields.find((f: any) => f.name === 'testPluginField'); + expect(testField).not.toBeNull(); expect(testField).not.toBeUndefined(); expect(testField?.name).toBe('testPluginField'); @@ -113,6 +122,7 @@ describe('graphile-test with plugins', () => { const typeName = testField.type?.name || testField.type?.ofType?.name || testField.type?.ofType?.ofType?.name; + expect(typeName).toBe('String'); }); }); @@ -155,6 +165,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.data?.anotherTestField).toBe('another-test-value'); expect(res.errors).toBeUndefined(); @@ -202,6 +213,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); @@ -249,6 +261,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.errors).toBeUndefined(); }); @@ -298,6 +311,7 @@ describe('graphile-test with plugins', () => { `; const res = await query(TEST_QUERY); + expect(res.data?.testPluginField).toBe('test-plugin-value'); expect(res.data?.anotherTestField).toBe('another-test-value'); expect(res.errors).toBeUndefined(); diff --git a/graphql/test/__tests__/graphile-test.roles.test.ts b/graphql/test/__tests__/graphile-test.roles.test.ts index 1b45bba95..755708c0d 100644 --- a/graphql/test/__tests__/graphile-test.roles.test.ts +++ b/graphql/test/__tests__/graphile-test.roles.test.ts @@ -1,13 +1,13 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import gql from 'graphql-tag'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; import { logDbSessionInfo } from '../test-utils/utils'; const schemas = ['app_public']; diff --git a/graphql/test/__tests__/graphile-test.test.ts b/graphql/test/__tests__/graphile-test.test.ts index 087e8cc33..57b92f7f5 100644 --- a/graphql/test/__tests__/graphile-test.test.ts +++ b/graphql/test/__tests__/graphile-test.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; import { IntrospectionQuery } from '../test-utils/queries'; import { logDbSessionInfo } from '../test-utils/utils'; @@ -39,5 +39,6 @@ afterAll(() => teardown()); it('introspection query snapshot', async () => { await logDbSessionInfo(db); const res = await query(IntrospectionQuery); + expect(snapshot(res)).toMatchSnapshot('introspection'); }); diff --git a/graphql/test/__tests__/graphile-test.types.positional.test.ts b/graphql/test/__tests__/graphile-test.types.positional.test.ts index f4c94afe4..c4571c6f8 100644 --- a/graphql/test/__tests__/graphile-test.types.positional.test.ts +++ b/graphql/test/__tests__/graphile-test.types.positional.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFn } from 'graphile-test'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnections } from '../src/get-connections'; -import type { GraphQLQueryFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphql/test/__tests__/graphile-test.types.positional.unwrapped.test.ts b/graphql/test/__tests__/graphile-test.types.positional.unwrapped.test.ts index d39136f2f..8dd1f8443 100644 --- a/graphql/test/__tests__/graphile-test.types.positional.unwrapped.test.ts +++ b/graphql/test/__tests__/graphile-test.types.positional.unwrapped.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryUnwrappedFn } from 'graphile-test'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsUnwrapped } from '../src/get-connections'; -import type { GraphQLQueryUnwrappedFn } from 'graphile-test'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphql/test/__tests__/graphile-test.types.test.ts b/graphql/test/__tests__/graphile-test.types.test.ts index 938b84a68..b91149b01 100644 --- a/graphql/test/__tests__/graphile-test.types.test.ts +++ b/graphql/test/__tests__/graphile-test.types.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryFnObj } from 'graphile-test'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsObject } from '../src/get-connections'; -import type { GraphQLQueryFnObj } from 'graphile-test'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphql/test/__tests__/graphile-test.types.unwrapped.test.ts b/graphql/test/__tests__/graphile-test.types.unwrapped.test.ts index e1975aa64..42a52b961 100644 --- a/graphql/test/__tests__/graphile-test.types.unwrapped.test.ts +++ b/graphql/test/__tests__/graphile-test.types.unwrapped.test.ts @@ -1,12 +1,12 @@ process.env.LOG_SCOPE = 'graphile-test'; +import type { GraphQLQueryUnwrappedFnObj } from 'graphile-test'; import { join } from 'path'; import { seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import { snapshot } from '../src/utils'; import { getConnectionsObjectUnwrapped } from '../src/get-connections'; -import type { GraphQLQueryUnwrappedFnObj } from 'graphile-test'; +import { snapshot } from '../src/utils'; const schemas = ['app_public']; const sql = (f: string) => join(__dirname, '/../sql', f); diff --git a/graphql/test/src/get-connections.ts b/graphql/test/src/get-connections.ts index d72ed2e47..24051a9ef 100644 --- a/graphql/test/src/get-connections.ts +++ b/graphql/test/src/get-connections.ts @@ -1,9 +1,3 @@ -import type { GetConnectionOpts, GetConnectionResult } from 'pgsql-test'; -import { getConnections as getPgConnections } from 'pgsql-test'; -import type { SeedAdapter } from 'pgsql-test/seed/types'; -import type { PgTestClient } from 'pgsql-test/test-client'; - -import { GraphQLTest } from './graphile-test'; import type { GetConnectionsInput, GraphQLQueryFn, @@ -13,6 +7,12 @@ import type { GraphQLQueryUnwrappedFnObj, GraphQLResponse } from 'graphile-test'; +import type { GetConnectionOpts, GetConnectionResult } from 'pgsql-test'; +import { getConnections as getPgConnections } from 'pgsql-test'; +import type { SeedAdapter } from 'pgsql-test/seed/types'; +import type { PgTestClient } from 'pgsql-test/test-client'; + +import { GraphQLTest } from './graphile-test'; // Core unwrapping utility const unwrap = (res: GraphQLResponse): T => { @@ -22,6 +22,7 @@ const unwrap = (res: GraphQLResponse): T => { if (!res.data) { throw new Error('No data returned from GraphQL query'); } + return res.data; }; @@ -34,6 +35,7 @@ const createConnectionsBase = async ( const { pg, db, teardown: dbTeardown } = conn; const gqlContext = GraphQLTest(input, conn); + await gqlContext.setup(); const teardown = async () => { @@ -121,7 +123,9 @@ export const getConnectionsObjectWithLogging = async ( const query: GraphQLQueryFnObj = async (opts) => { console.log('Executing GraphQL query:', opts.query); const result = await baseQuery(opts); + console.log('GraphQL result:', result); + return result; }; @@ -151,7 +155,9 @@ export const getConnectionsObjectWithTiming = async ( const start = Date.now(); const result = await baseQuery(opts); const duration = Date.now() - start; + console.log(`GraphQL query took ${duration}ms`); + return result; }; @@ -231,7 +237,9 @@ export const getConnectionsWithLogging = async ( const query: GraphQLQueryFn = async (query, variables, commit, reqOptions) => { console.log('Executing positional GraphQL query:', query); const result = await baseQueryPositional(query, variables, commit, reqOptions); + console.log('GraphQL result:', result); + return result; }; @@ -261,7 +269,9 @@ export const getConnectionsWithTiming = async ( const start = Date.now(); const result = await baseQueryPositional(query, variables, commit, reqOptions); const duration = Date.now() - start; + console.log(`Positional GraphQL query took ${duration}ms`); + return result; }; diff --git a/graphql/test/src/graphile-test.ts b/graphql/test/src/graphile-test.ts index 7cae97231..e3c52de8a 100644 --- a/graphql/test/src/graphile-test.ts +++ b/graphql/test/src/graphile-test.ts @@ -1,9 +1,9 @@ -import type { GraphQLQueryOptions, GraphQLTestContext, GetConnectionsInput } from 'graphile-test'; import { getGraphileSettings } from 'graphile-settings'; +import type { GetConnectionsInput,GraphQLQueryOptions, GraphQLTestContext } from 'graphile-test'; +import { runGraphQLInContext } from 'graphile-test/context'; import type { GraphQLSchema } from 'graphql'; import type { GetConnectionOpts, GetConnectionResult } from 'pgsql-test'; import { createPostGraphileSchema, PostGraphileOptions } from 'postgraphile'; -import { runGraphQLInContext } from 'graphile-test/context'; export const GraphQLTest = ( input: GetConnectionsInput & GetConnectionOpts, diff --git a/graphql/test/src/index.ts b/graphql/test/src/index.ts index dbea2fa5e..5b7de8640 100644 --- a/graphql/test/src/index.ts +++ b/graphql/test/src/index.ts @@ -1,16 +1,16 @@ // Re-export types and utilities from graphile-test (but not get-connections functions) export { - type GraphQLQueryOptions, - type GraphQLTestContext, type GetConnectionsInput, - type GraphQLResponse, type GraphQLQueryFn, type GraphQLQueryFnObj, + type GraphQLQueryOptions, type GraphQLQueryUnwrappedFn, type GraphQLQueryUnwrappedFnObj, + type GraphQLResponse, + type GraphQLTestContext, } from 'graphile-test'; // Override with our custom implementations that use graphile-settings -export { GraphQLTest } from './graphile-test'; export * from './get-connections'; +export { GraphQLTest } from './graphile-test'; export { seed, snapshot } from 'pgsql-test'; \ No newline at end of file diff --git a/graphql/test/test-utils/utils.ts b/graphql/test/test-utils/utils.ts index bef9b854f..b792c423d 100644 --- a/graphql/test/test-utils/utils.ts +++ b/graphql/test/test-utils/utils.ts @@ -6,6 +6,7 @@ export const logDbSessionInfo = async (db: { query: (sql: string) => Promise unknown>( result = fn.apply((context ?? this) as never, args) as ReturnType; called = true; } + return result; }; } @@ -40,10 +41,12 @@ const end = (pool: Pool): void => { ended?: boolean; ending?: boolean; }; + if (state.ended || state.ending) { console.error( 'DO NOT CLOSE pool, why are you trying to call end() when already ended?' ); + return; } void pool.end(); diff --git a/jobs/job-scheduler/src/index.ts b/jobs/job-scheduler/src/index.ts index a3d8df160..ed765fd81 100644 --- a/jobs/job-scheduler/src/index.ts +++ b/jobs/job-scheduler/src/index.ts @@ -1,9 +1,9 @@ -import * as jobs from '@constructive-io/job-utils'; +import poolManager from '@constructive-io/job-pg'; import type { PgClientLike } from '@constructive-io/job-utils'; +import * as jobs from '@constructive-io/job-utils'; +import { Logger } from '@pgpmjs/logger'; import schedule from 'node-schedule'; -import poolManager from '@constructive-io/job-pg'; import type { Pool, PoolClient } from 'pg'; -import { Logger } from '@pgpmjs/logger'; export interface ScheduledJobRow { id: number | string; @@ -58,6 +58,7 @@ export default class Scheduler { }); }); } + async initialize(client: PgClientLike) { if (this._initialized === true) return; await jobs.releaseScheduledJobs(client, { @@ -68,6 +69,7 @@ export default class Scheduler { this._initialized = true; await this.doNext(client); } + async handleFatalError( client: PgClientLike, { @@ -77,11 +79,13 @@ export default class Scheduler { }: { err?: Error; fatalError: unknown; jobId: ScheduledJobRow['id'] } ) { const when = err ? `after failure '${err.message}'` : 'after success'; + log.error(`Failed to release job '${jobId}' ${when}; committing seppuku`); log.error(String(fatalError)); await poolManager.close(); process.exit(1); } + async handleError( client: PgClientLike, { @@ -94,12 +98,14 @@ export default class Scheduler { `Failed to initialize scheduler for ${job.id} (${job.task_identifier}) with error ${err.message} (${duration}ms)` ); const j = this.jobs[job.id]; + if (j) j.cancel(); await jobs.releaseScheduledJobs(client, { workerId: this.workerId, ids: [job.id] }); } + async handleSuccess( client: PgClientLike, { job, duration }: { job: ScheduledJobRow; duration: string } @@ -108,6 +114,7 @@ export default class Scheduler { `initialized ${job.id} (${job.task_identifier}) with success (${duration}ms)` ); } + async scheduleJob(client: PgClientLike, job: ScheduledJobRow) { const { id, task_identifier, schedule_info } = job; const j = schedule.scheduleJob(schedule_info as never, async () => { @@ -124,6 +131,7 @@ export default class Scheduler { `attempted job[${job.task_identifier}] but it's probably non existent, unscheduling...` ); const scheduledJob = this.jobs[job.id]; + if (scheduledJob) scheduledJob.cancel(); } } else { @@ -132,8 +140,10 @@ export default class Scheduler { ); } }); + this.jobs[id] = j as SchedulerJobHandle; } + async doNext(client: PgClientLike): Promise { if (!this._initialized) { return await this.initialize(client); @@ -150,16 +160,19 @@ export default class Scheduler { ? null : this.supportedTaskNames }); + if (!job || !job.id) { this.doNextTimer = setTimeout( () => this.doNext(client), this.idleDelay ); + return; } const start = process.hrtime(); let err: Error | null = null; + try { await this.scheduleJob(client, job); } catch (error) { @@ -171,6 +184,7 @@ export default class Scheduler { 2 ); const jobId = job.id; + try { if (err) { await this.handleError(client, { err, job, duration }); @@ -180,11 +194,13 @@ export default class Scheduler { } catch (fatalError: unknown) { await this.handleFatalError(client, { err, fatalError, jobId }); } + return this.doNext(client); } catch (err: unknown) { this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay); } } + listen() { const listenForChanges = ( err: Error | null, @@ -199,6 +215,7 @@ export default class Scheduler { // Try again in 5 seconds // should this really be done in the node process? setTimeout(this.listen, 5000); + return; } client.on('notification', () => { @@ -222,6 +239,7 @@ export default class Scheduler { ); this.doNext(client); }; + this.pgPool.connect(listenForChanges); } } diff --git a/jobs/job-scheduler/src/run.ts b/jobs/job-scheduler/src/run.ts index ae4bda1f8..3b4bf22e5 100644 --- a/jobs/job-scheduler/src/run.ts +++ b/jobs/job-scheduler/src/run.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node -import Scheduler from './index'; import poolManager from '@constructive-io/job-pg'; import { - getSchedulerHostname, - getJobSupported -} from '@constructive-io/job-utils'; + getJobSupported, + getSchedulerHostname} from '@constructive-io/job-utils'; + +import Scheduler from './index'; const pgPool = poolManager.getPool(); diff --git a/jobs/job-utils/src/index.ts b/jobs/job-utils/src/index.ts index ab8b70f04..2df83fc8c 100644 --- a/jobs/job-utils/src/index.ts +++ b/jobs/job-utils/src/index.ts @@ -1,30 +1,27 @@ +import { Logger } from '@pgpmjs/logger'; import type { - FailJobParams, CompleteJobParams, + FailJobParams, GetJobParams, GetScheduledJobParams, - RunScheduledJobParams, + ReleaseJobsParams, ReleaseScheduledJobsParams, - ReleaseJobsParams -} from '@pgpmjs/types'; + RunScheduledJobParams} from '@pgpmjs/types'; import { - getJobSchema, + getCallbackBaseUrl, + getJobConnectionString, + getJobGatewayConfig, + getJobGatewayDevMap, getJobPgConfig, getJobPool, - getJobConnectionString, + getJobsCallbackPort, + getJobSchema, getJobSupportAny, getJobSupported, - getWorkerHostname, + getNodeEnvironment, getSchedulerHostname, - getJobGatewayConfig, - getJobGatewayDevMap, - getJobsCallbackPort, - getCallbackBaseUrl, - getNodeEnvironment -} from './runtime'; - -import { Logger } from '@pgpmjs/logger'; + getWorkerHostname} from './runtime'; const log = new Logger('jobs:core'); @@ -33,20 +30,19 @@ export type PgClientLike = { }; export { - getJobSchema, + getCallbackBaseUrl, + getJobConnectionString, + getJobGatewayConfig, + getJobGatewayDevMap, getJobPgConfig, getJobPool, - getJobConnectionString, + getJobsCallbackPort, + getJobSchema, getJobSupportAny, getJobSupported, - getWorkerHostname, + getNodeEnvironment, getSchedulerHostname, - getJobGatewayConfig, - getJobGatewayDevMap, - getJobsCallbackPort, - getCallbackBaseUrl, - getNodeEnvironment -}; + getWorkerHostname}; const JOBS_SCHEMA = getJobSchema(); @@ -83,6 +79,7 @@ export const getJob = async ( `SELECT * FROM "${JOBS_SCHEMA}".get_job($1, $2::text[]);`, [workerId, supportedTaskNames] ); + return (job as T) ?? null; }; @@ -97,6 +94,7 @@ export const getScheduledJob = async ( `SELECT * FROM "${JOBS_SCHEMA}".get_scheduled_job($1, $2::text[]);`, [workerId, supportedTaskNames] ); + return (job as T) ?? null; }; @@ -112,6 +110,7 @@ export const runScheduledJob = async ( `SELECT * FROM "${JOBS_SCHEMA}".run_scheduled_job($1);`, [jobId] ); + return job ?? null; } catch (e: any) { if (e?.message === 'ALREADY_SCHEDULED') { @@ -126,6 +125,7 @@ export const releaseScheduledJobs = async ( { workerId, ids }: ReleaseScheduledJobsParams ) => { log.info(`releaseScheduledJobs worker[${workerId}]`); + return client.query( `SELECT "${JOBS_SCHEMA}".release_scheduled_jobs($1, $2::bigint[]);`, [workerId, ids ?? null] @@ -137,6 +137,7 @@ export const releaseJobs = async ( { workerId }: ReleaseJobsParams ) => { log.info(`releaseJobs worker[${workerId}]`); + return client.query( `SELECT "${JOBS_SCHEMA}".release_jobs($1);`, [workerId] diff --git a/jobs/job-utils/src/runtime.ts b/jobs/job-utils/src/runtime.ts index 689d14bbe..0761601d0 100644 --- a/jobs/job-utils/src/runtime.ts +++ b/jobs/job-utils/src/runtime.ts @@ -1,9 +1,9 @@ import { getEnvOptions, getNodeEnv, parseEnvBoolean } from '@pgpmjs/env'; -import { defaultPgConfig, getPgEnvVars, type PgConfig } from 'pg-env'; -import { getPgPool } from 'pg-cache'; -import type { Pool } from 'pg'; import type { PgpmOptions } from '@pgpmjs/types'; import { jobsDefaults } from '@pgpmjs/types'; +import type { Pool } from 'pg'; +import { getPgPool } from 'pg-cache'; +import { defaultPgConfig, getPgEnvVars, type PgConfig } from 'pg-env'; type Maybe = T | null | undefined; @@ -31,6 +31,7 @@ export const getJobConnectionString = (): string => { const auth = cfg.user ? `${cfg.user}${cfg.password ? `:${cfg.password}` : ''}@` : ''; + return `postgres://${auth}${cfg.host}:${cfg.port}/${cfg.database}`; }; @@ -38,6 +39,7 @@ export const getJobConnectionString = (): string => { export const getJobSchema = (): string => { const opts: PgpmOptions = getEnvOptions(); const fromOpts: string | undefined = opts.jobs?.schema?.schema; + return ( fromOpts || jobsDefaults.schema?.schema || @@ -49,6 +51,7 @@ export const getJobSchema = (): string => { export const getJobSupportAny = (): boolean => { const opts: PgpmOptions = getEnvOptions(); const envVal = parseEnvBoolean(process.env.JOBS_SUPPORT_ANY); + if (typeof envVal === 'boolean') return envVal; const worker: boolean | undefined = opts.jobs?.worker?.supportAny; @@ -78,6 +81,7 @@ export const getJobSupported = (): string[] => { // ---- Hostnames ---- export const getWorkerHostname = (): string => { const opts: PgpmOptions = getEnvOptions(); + return ( process.env.HOSTNAME || opts.jobs?.worker?.hostname || @@ -88,6 +92,7 @@ export const getWorkerHostname = (): string => { export const getSchedulerHostname = (): string => { const opts: PgpmOptions = getEnvOptions(); + return ( process.env.HOSTNAME || opts.jobs?.scheduler?.hostname || @@ -123,6 +128,7 @@ export const getJobGatewayDevMap = (): | Record | null => { const map = process.env.INTERNAL_GATEWAY_DEVELOPMENT_MAP; + if (!map) return null; try { return JSON.parse(map); @@ -133,6 +139,7 @@ export const getJobGatewayDevMap = (): 'Value:', map ); + return null; } }; @@ -142,6 +149,7 @@ export const getNodeEnvironment = getNodeEnv; // Neutral callback helpers (generic HTTP callback) export const getJobsCallbackPort = (): number => { const { callbackPort } = getJobGatewayConfig(); + return callbackPort; }; @@ -151,5 +159,6 @@ export const getCallbackBaseUrl = (): string => { } const host = process.env.JOBS_CALLBACK_HOST || 'jobs-callback'; const port = getJobsCallbackPort(); + return `http://${host}:${port}/callback`; }; diff --git a/jobs/job-worker/src/index.ts b/jobs/job-worker/src/index.ts index 6e3598e00..ee7397c4d 100644 --- a/jobs/job-worker/src/index.ts +++ b/jobs/job-worker/src/index.ts @@ -1,7 +1,7 @@ -import pg from 'pg'; -import type { Pool, PoolClient } from 'pg'; -import * as jobs from '@constructive-io/job-utils'; import type { PgClientLike } from '@constructive-io/job-utils'; +import * as jobs from '@constructive-io/job-utils'; +import type { Pool, PoolClient } from 'pg'; +import pg from 'pg'; const pgPoolConfig = { connectionString: jobs.getJobConnectionString() @@ -12,11 +12,13 @@ function once unknown>( context?: unknown ): (...args: Parameters) => ReturnType | undefined { let result: ReturnType | undefined; + return function (this: unknown, ...args: Parameters) { if (fn) { result = fn.apply((context ?? this) as never, args) as ReturnType; fn = null as unknown as T; } + return result; }; } @@ -37,7 +39,7 @@ export type TaskHandler = ( job: JobRow ) => Promise | void; -/* eslint-disable no-console */ + export default class Worker { tasks: Record; @@ -78,15 +80,18 @@ export default class Worker { console.log('closing connection...'); this.close(); }; + process.once('SIGTERM', close); process.once('SIGINT', close); } + close() { if (!this._ended) { this.pgPool.end(); } this._ended = true; } + handleFatalError({ err, fatalError, @@ -97,12 +102,14 @@ export default class Worker { jobId: JobRow['id']; }) { const when = err ? `after failure '${err.message}'` : 'after success'; + console.error( `Failed to release job '${jobId}' ${when}; committing seppuku` ); console.error(fatalError); process.exit(1); } + async handleError( client: PgClientLike, { err, job, duration }: { err: Error; job: JobRow; duration: string } @@ -118,6 +125,7 @@ export default class Worker { message: err.message }); } + async handleSuccess( client: PgClientLike, { job, duration }: { job: JobRow; duration: string } @@ -127,9 +135,11 @@ export default class Worker { ); await jobs.completeJob(client, { workerId: this.workerId, jobId: job.id }); } + async doWork(job: JobRow) { const { task_identifier } = job; const worker = this.tasks[task_identifier]; + if (!worker) { throw new Error('Unsupported task'); } @@ -141,6 +151,7 @@ export default class Worker { job ); } + async doNext(client: PgClientLike): Promise { if (this.doNextTimer) { clearTimeout(this.doNextTimer); @@ -151,16 +162,19 @@ export default class Worker { workerId: this.workerId, supportedTaskNames: this.supportedTaskNames })) as JobRow | undefined; + if (!job || !job.id) { this.doNextTimer = setTimeout( () => this.doNext(client), this.idleDelay ); + return; } const start = process.hrtime(); let err: Error | null = null; + try { await this.doWork(job); } catch (error) { @@ -171,6 +185,7 @@ export default class Worker { 2 ); const jobId = job.id; + try { if (err) { await this.handleError(client, { err, job, duration }); @@ -180,11 +195,13 @@ export default class Worker { } catch (fatalError: unknown) { this.handleFatalError({ err, fatalError, jobId }); } + return this.doNext(client); } catch (err: unknown) { this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay); } } + listen() { const listenForChanges = ( err: Error | null, @@ -196,6 +213,7 @@ export default class Worker { // Try again in 5 seconds // should this really be done in the node process? setTimeout(this.listen, 5000); + return; } client.on('notification', () => { @@ -213,6 +231,7 @@ export default class Worker { console.log(`${this.workerId} connected and looking for jobs...`); this.doNext(client); }; + this.pgPool.connect(listenForChanges); } } diff --git a/jobs/job-worker/src/run.ts b/jobs/job-worker/src/run.ts index 358d44938..2581fa130 100644 --- a/jobs/job-worker/src/run.ts +++ b/jobs/job-worker/src/run.ts @@ -1,4 +1,4 @@ -import Worker, { WorkerContext, JobRow } from './index'; +import Worker, { JobRow,WorkerContext } from './index'; const worker = new Worker({ tasks: { diff --git a/jobs/knative-job-example/src/index.ts b/jobs/knative-job-example/src/index.ts index e3cbd1ce2..c11b9b731 100644 --- a/jobs/knative-job-example/src/index.ts +++ b/jobs/knative-job-example/src/index.ts @@ -13,4 +13,5 @@ app.post('*', async (req: any, res: any, next: any) => { }); const port = Number(process.env.PORT) || 10101; + app.listen(port); diff --git a/jobs/knative-job-fn/src/index.ts b/jobs/knative-job-fn/src/index.ts index 77befd2b1..86cdcd018 100644 --- a/jobs/knative-job-fn/src/index.ts +++ b/jobs/knative-job-fn/src/index.ts @@ -1,9 +1,10 @@ -import express from 'express'; -import bodyParser from 'body-parser'; import http from 'node:http'; import https from 'node:https'; import { URL } from 'node:url'; +import bodyParser from 'body-parser'; +import express from 'express'; + type JobCallbackStatus = 'success' | 'error'; type JobContext = { @@ -33,6 +34,7 @@ app.use((req: any, res: any, next: any) => { const headers = getHeaders(req); let body: any; + if (req.body && typeof req.body === 'object') { // Only log top-level keys to avoid exposing sensitive body contents. body = { keys: Object.keys(req.body) }; @@ -43,7 +45,7 @@ app.use((req: any, res: any, next: any) => { body = undefined; } - // eslint-disable-next-line no-console + console.log('[knative-job-fn] Incoming job request', { method: req.method, path: req.originalUrl || req.url, @@ -71,9 +73,11 @@ app.use((req: any, res: any, next: any) => { const normalizeCallbackUrl = (rawUrl: string): string => { try { const url = new URL(rawUrl); + if (!url.pathname || url.pathname === '/') { url.pathname = '/callback'; } + return url.toString(); } catch { return rawUrl; @@ -87,6 +91,7 @@ const postJson = ( ): Promise => { return new Promise((resolve, reject) => { let url: URL; + try { url = new URL(urlStr); } catch (e) { @@ -127,6 +132,7 @@ const sendJobCallback = async ( errorMessage?: string ) => { const { callbackUrl, workerId, jobId, databaseId } = ctx; + if (!callbackUrl || !workerId || !jobId) { return; } @@ -152,7 +158,7 @@ const sendJobCallback = async ( } try { - // eslint-disable-next-line no-console + console.log('[knative-job-fn] Sending job callback', { status, target: normalizeCallbackUrl(callbackUrl), @@ -162,7 +168,7 @@ const sendJobCallback = async ( }); await postJson(target, headers, body); } catch (err) { - // eslint-disable-next-line no-console + console.error('[knative-job-fn] Failed to POST job callback', { target, status, @@ -190,7 +196,7 @@ app.use((req: any, res: any, next: any) => { // If an error handler already sent a callback, skip. if (res.locals.jobCallbackSent) return; res.locals.jobCallbackSent = true; - // eslint-disable-next-line no-console + console.log('[knative-job-fn] Function completed', { workerId: ctx.workerId, jobId: ctx.jobId, @@ -205,14 +211,14 @@ app.use((req: any, res: any, next: any) => { }); export default { - post: function (...args: any[]) { + post (...args: any[]) { return app.post.apply(app, args as any); }, listen: (port: any, cb: () => void = () => {}) => { // NOTE Remember that Express middleware executes in order. // You should define error handlers last, after all other middleware. // Otherwise, your error handler won't get called - // eslint-disable-next-line no-unused-vars + app.use(async (error: any, req: any, res: any, next: any) => { res.set({ 'Content-Type': 'application/json', @@ -222,12 +228,13 @@ export default { // Mark job as having errored via callback, if available. try { const ctx: JobContext | undefined = res.locals?.jobContext; + if (ctx && !res.locals.jobCallbackSent) { res.locals.jobCallbackSent = true; await sendJobCallback(ctx, 'error', error?.message); } } catch (err) { - // eslint-disable-next-line no-console + console.error('[knative-job-fn] Failed to send error callback', err); } @@ -251,7 +258,7 @@ export default { }; } - // eslint-disable-next-line no-console + console.error('[knative-job-fn] Function error', { headers, path: req.originalUrl || req.url, diff --git a/jobs/knative-job-server/src/index.ts b/jobs/knative-job-server/src/index.ts index 10109a871..e292c1d7c 100644 --- a/jobs/knative-job-server/src/index.ts +++ b/jobs/knative-job-server/src/index.ts @@ -1,8 +1,8 @@ -import express from 'express'; +import poolManager from '@constructive-io/job-pg'; +import * as jobs from '@constructive-io/job-utils'; import bodyParser from 'body-parser'; +import express from 'express'; import type { Pool, PoolClient } from 'pg'; -import * as jobs from '@constructive-io/job-utils'; -import poolManager from '@constructive-io/job-pg'; type JobRequestBody = { message?: string; @@ -36,12 +36,14 @@ type WithClientHandler = ( export default (pgPool: Pool = poolManager.getPool()) => { const app = express(); + app.use(bodyParser.json()); const withClient = (cb: WithClientHandler) => async (req: JobRequest, res: JobResponse, next: NextFn) => { const client = (await pgPool.connect()) as PoolClient; + try { await cb(client as PoolClient, req, res, next); } catch (e) { @@ -61,6 +63,7 @@ export default (pgPool: Pool = poolManager.getPool()) => { { workerId, jobIdHeader } ); res.status(400).json({ error: 'Missing X-Worker-Id or X-Job-Id header' }); + return; } @@ -85,6 +88,7 @@ export default (pgPool: Pool = poolManager.getPool()) => { { workerId, jobIdHeader } ); res.status(400).json({ error: 'Missing X-Worker-Id or X-Job-Id header' }); + return; } @@ -110,6 +114,7 @@ export default (pgPool: Pool = poolManager.getPool()) => { console.log('server: undefined JOB, what is this? healthcheck?'); console.log(req.url); console.log(req.originalUrl); + return res.status(200).json('OK'); } diff --git a/jobs/knative-job-server/src/run.ts b/jobs/knative-job-server/src/run.ts index 5eed56f4e..b9550d809 100755 --- a/jobs/knative-job-server/src/run.ts +++ b/jobs/knative-job-server/src/run.ts @@ -1,13 +1,14 @@ #!/usr/bin/env node -import server from './index'; import poolManager from '@constructive-io/job-pg'; import { getJobsCallbackPort } from '@constructive-io/job-utils'; +import server from './index'; + const pgPool = poolManager.getPool(); const port = getJobsCallbackPort(); server(pgPool).listen(port, () => { - // eslint-disable-next-line no-console + console.log(`listening ON ${port}`); }); diff --git a/jobs/knative-job-service/src/run.ts b/jobs/knative-job-service/src/run.ts index 33caf9737..5ed7b8ff0 100755 --- a/jobs/knative-job-service/src/run.ts +++ b/jobs/knative-job-service/src/run.ts @@ -1,29 +1,29 @@ #!/usr/bin/env node -import Scheduler from '@constructive-io/job-scheduler'; -import Worker from '@constructive-io/knative-job-worker'; -import server from '@constructive-io/knative-job-server'; import poolManager from '@constructive-io/job-pg'; -import { Client } from 'pg'; -import retry from 'async-retry'; +import Scheduler from '@constructive-io/job-scheduler'; import { getJobPgConfig, + getJobsCallbackPort, getJobSchema, + getJobSupported, getSchedulerHostname, getWorkerHostname, - getJobSupported, - getJobsCallbackPort, } from '@constructive-io/job-utils'; +import server from '@constructive-io/knative-job-server'; +import Worker from '@constructive-io/knative-job-worker'; +import retry from 'async-retry'; +import { Client } from 'pg'; export const startJobsServices = () => { - // eslint-disable-next-line no-console + console.log('starting jobs services...'); const pgPool = poolManager.getPool(); const app = server(pgPool); const callbackPort = getJobsCallbackPort(); const httpServer = app.listen(callbackPort, () => { - // eslint-disable-next-line no-console + console.log(`[cb] listening ON ${callbackPort}`); const tasks = getJobSupported(); @@ -48,11 +48,13 @@ export const startJobsServices = () => { }; export const waitForJobsPrereqs = async (): Promise => { - // eslint-disable-next-line no-console + console.log('waiting for jobs prereqs'); let client: Client | null = null; + try { const cfg = getJobPgConfig(); + client = new Client({ host: cfg.host, port: cfg.port, @@ -62,9 +64,10 @@ export const waitForJobsPrereqs = async (): Promise => { }); await client.connect(); const schema = getJobSchema(); + await client.query(`SELECT * FROM "${schema}".jobs LIMIT 1;`); } catch (error) { - // eslint-disable-next-line no-console + console.log(error); throw new Error('jobs server boot failed...'); } finally { @@ -75,7 +78,7 @@ export const waitForJobsPrereqs = async (): Promise => { }; export const bootJobs = async (): Promise => { - // eslint-disable-next-line no-console + console.log('attempting to boot jobs'); await retry( async () => { diff --git a/jobs/knative-job-worker/__tests__/req.test.ts b/jobs/knative-job-worker/__tests__/req.test.ts index c57af4951..2236d431c 100644 --- a/jobs/knative-job-worker/__tests__/req.test.ts +++ b/jobs/knative-job-worker/__tests__/req.test.ts @@ -73,6 +73,7 @@ describe('knative request wrapper', () => { }); const [options] = postMock.mock.calls[0]; + expect(options.url).toBe( 'http://gateway.internal/async-function/example-fn' ); @@ -99,6 +100,7 @@ describe('knative request wrapper', () => { }); const [options] = postMock.mock.calls[0]; + expect(options.url).toBe('http://localhost:3000/dev-fn'); }); diff --git a/jobs/knative-job-worker/__tests__/worker.integration.test.ts b/jobs/knative-job-worker/__tests__/worker.integration.test.ts index 68ceff214..0bcb8b475 100644 --- a/jobs/knative-job-worker/__tests__/worker.integration.test.ts +++ b/jobs/knative-job-worker/__tests__/worker.integration.test.ts @@ -21,10 +21,11 @@ jest.mock('@constructive-io/job-pg', () => ({ } })); +import * as jobUtils from '@constructive-io/job-utils'; import path from 'path'; import { getConnections, seed } from 'pgsql-test'; import type { PgTestClient } from 'pgsql-test/test-client'; -import * as jobUtils from '@constructive-io/job-utils'; + import Worker from '../src'; let db: PgTestClient; @@ -35,6 +36,7 @@ beforeAll(async () => { __dirname, '../../../extensions/@pgpm/database-jobs' ); + ({ db, teardown } = await getConnections({}, [seed.loadPgpm(modulePath)])); db.setContext({ role: 'administrator' }); }); @@ -83,6 +85,7 @@ describe('knative worker integration with job queue', () => { expect(postMock).toHaveBeenCalledTimes(1); const [options] = postMock.mock.calls[0]; + expect(options.url).toBe('http://example-fn.knative.internal'); expect(options.headers['X-Job-Id']).toBe(job.id); expect(options.headers['X-Database-Id']).toBe(databaseId); diff --git a/jobs/knative-job-worker/src/index.ts b/jobs/knative-job-worker/src/index.ts index 6599aab29..e5c31aac6 100644 --- a/jobs/knative-job-worker/src/index.ts +++ b/jobs/knative-job-worker/src/index.ts @@ -1,8 +1,9 @@ import poolManager from '@constructive-io/job-pg'; -import * as jobs from '@constructive-io/job-utils'; import type { PgClientLike } from '@constructive-io/job-utils'; -import type { Pool, PoolClient } from 'pg'; +import * as jobs from '@constructive-io/job-utils'; import { Logger } from '@pgpmjs/logger'; +import type { Pool, PoolClient } from 'pg'; + import { request as req } from './req'; export interface JobRow { @@ -50,6 +51,7 @@ export default class Worker { await jobs.releaseJobs(pgPool, { workerId: this.workerId }); }); } + async initialize(client: PgClientLike) { if (this._initialized === true) return; @@ -59,6 +61,7 @@ export default class Worker { this._initialized = true; await this.doNext(client); } + async handleFatalError( client: PgClientLike, { @@ -68,11 +71,13 @@ export default class Worker { }: { err?: Error; fatalError: unknown; jobId: JobRow['id'] } ) { const when = err ? `after failure '${err.message}'` : 'after success'; + log.error(`Failed to release job '${jobId}' ${when}; committing seppuku`); await poolManager.close(); log.error(String(fatalError)); process.exit(1); } + async handleError( client: PgClientLike, { err, job, duration }: { err: Error; job: JobRow; duration: string } @@ -89,6 +94,7 @@ export default class Worker { message: err.message }); } + async handleSuccess( client: PgClientLike, { job, duration }: { job: JobRow; duration: string } @@ -97,8 +103,10 @@ export default class Worker { `Async task ${job.id} (${job.task_identifier}) to be processed` ); } + async doWork(job: JobRow) { const { payload, task_identifier } = job; + log.debug('starting work on job', { id: job.id, task: task_identifier, @@ -117,6 +125,7 @@ export default class Worker { jobId: job.id }); } + async doNext(client: PgClientLike): Promise { if (!this._initialized) { return await this.initialize(client); @@ -140,11 +149,13 @@ export default class Worker { () => this.doNext(client), this.idleDelay ); + return; } const start = process.hrtime(); let err: Error | null = null; + try { await this.doWork(job); } catch (error) { @@ -155,6 +166,7 @@ export default class Worker { 2 ); const jobId = job.id; + try { if (err) { await this.handleError(client, { err, job, duration }); @@ -164,11 +176,13 @@ export default class Worker { } catch (fatalError: unknown) { await this.handleFatalError(client, { err, fatalError, jobId }); } + return this.doNext(client); } catch (err: unknown) { this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay); } } + listen() { const listenForChanges = ( err: Error | null, @@ -183,6 +197,7 @@ export default class Worker { // Try again in 5 seconds // should this really be done in the node process? setTimeout(this.listen, 5000); + return; } client.on('notification', () => { @@ -203,6 +218,7 @@ export default class Worker { log.info(`${this.workerId} connected and looking for jobs...`); this.doNext(client); }; + this.pgPool.connect(listenForChanges); } } diff --git a/jobs/knative-job-worker/src/req.ts b/jobs/knative-job-worker/src/req.ts index 0fe4e63a5..b12003220 100644 --- a/jobs/knative-job-worker/src/req.ts +++ b/jobs/knative-job-worker/src/req.ts @@ -1,4 +1,3 @@ -import requestLib from 'request'; import { getCallbackBaseUrl, getJobGatewayConfig, @@ -6,6 +5,7 @@ import { getNodeEnvironment } from '@constructive-io/job-utils'; import { Logger } from '@pgpmjs/logger'; +import requestLib from 'request'; const log = new Logger('jobs:req'); @@ -23,6 +23,7 @@ const getFunctionUrl = (fn: string): string => { const { gatewayUrl } = getJobGatewayConfig(); const base = gatewayUrl.replace(/\/$/, ''); + return `${base}/${fn}`; }; @@ -38,6 +39,7 @@ const request = ( { body, databaseId, workerId, jobId }: RequestOptions ) => { const url = getFunctionUrl(fn); + log.info(`dispatching job`, { fn, url, @@ -46,6 +48,7 @@ const request = ( jobId, databaseId }); + return new Promise((resolve, reject) => { requestLib.post( { @@ -64,15 +67,17 @@ const request = ( json: true, body }, - function (error: unknown) { + (error: unknown) => { if (error) { log.error(`request error for job[${jobId}] fn[${fn}]`, error); if (error instanceof Error && error.stack) { log.debug(error.stack); } + return reject(error); } log.debug(`request success for job[${jobId}] fn[${fn}]`); + return resolve(true); } ); diff --git a/jobs/knative-job-worker/src/run.ts b/jobs/knative-job-worker/src/run.ts index 3b2668b29..538b8e61c 100755 --- a/jobs/knative-job-worker/src/run.ts +++ b/jobs/knative-job-worker/src/run.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node -import Worker from './index'; import poolManager from '@constructive-io/job-pg'; import { - getWorkerHostname, - getJobSupported -} from '@constructive-io/job-utils'; + getJobSupported, + getWorkerHostname} from '@constructive-io/job-utils'; + +import Worker from './index'; const pgPool = poolManager.getPool(); diff --git a/package.json b/package.json index 979e5d93c..ee5ad57a1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "deps": "pnpm up -r -i -L" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@jest/test-sequencer": "^30.2.0", "@types/jest": "^30.0.0", "@types/jest-in-case": "^1.0.9", diff --git a/packages/cli/__tests__/add.test.ts b/packages/cli/__tests__/add.test.ts index d1d93a101..ba1f4ab56 100644 --- a/packages/cli/__tests__/add.test.ts +++ b/packages/cli/__tests__/add.test.ts @@ -52,6 +52,7 @@ describe('cmds:add', () => { const relativeFiles = absoluteFiles .map((file) => path.relative(argv.cwd, file)) .sort(); + argv.cwd = ''; expect(argv).toMatchSnapshot(`${label} - argv`); @@ -63,6 +64,7 @@ describe('cmds:add', () => { const setupModule = async (moduleDir: string) => { const moduleName = path.basename(moduleDir); + fs.mkdirSync(moduleDir, { recursive: true }); fs.mkdirSync(path.join(moduleDir, 'deploy'), { recursive: true }); fs.mkdirSync(path.join(moduleDir, 'revert'), { recursive: true }); @@ -73,6 +75,7 @@ describe('cmds:add', () => { %uri=https://github.com/test/${moduleName} `; + fs.writeFileSync(path.join(moduleDir, 'pgpm.plan'), planContent); const controlContent = `# ${moduleName} extension @@ -81,11 +84,13 @@ default_version = '0.1.0' relocatable = false superuser = false `; + fs.writeFileSync(path.join(moduleDir, `${moduleName}.control`), controlContent); }; it('adds simple change', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module'); + await setupModule(moduleDir); await runAddTest( @@ -101,11 +106,13 @@ superuser = false expect(fs.existsSync(path.join(moduleDir, 'verify', 'organizations.sql'))).toBe(true); const planContent = fs.readFileSync(path.join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('organizations'); }); it('adds change with note', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-with-note'); + await setupModule(moduleDir); await runAddTest( @@ -118,12 +125,14 @@ superuser = false ); const planContent = fs.readFileSync(path.join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('brands'); expect(planContent).toContain('Adds the brands table'); }); it('adds change with dependencies', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-with-deps'); + await setupModule(moduleDir); await runAddTest( @@ -145,15 +154,18 @@ superuser = false ); const planContent = fs.readFileSync(path.join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('users'); expect(planContent).toContain('contacts'); const deployContent = fs.readFileSync(path.join(moduleDir, 'deploy', 'contacts.sql'), 'utf8'); + expect(deployContent).toContain('requires: users'); }); it('adds nested path change', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-nested'); + await setupModule(moduleDir); await runAddTest( @@ -169,11 +181,13 @@ superuser = false expect(fs.existsSync(path.join(moduleDir, 'verify', 'api', 'v1', 'endpoints.sql'))).toBe(true); const planContent = fs.readFileSync(path.join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('api/v1/endpoints'); }); it('adds deeply nested path change', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-deep-nested'); + await setupModule(moduleDir); await runAddTest( @@ -189,12 +203,14 @@ superuser = false expect(fs.existsSync(path.join(moduleDir, 'verify', 'schema', 'myschema', 'tables', 'mytable.sql'))).toBe(true); const planContent = fs.readFileSync(path.join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('schema/myschema/tables/mytable'); }); it('shows help when requested', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-help'); + await setupModule(moduleDir); const { mockInput, mockOutput } = environment; @@ -206,6 +222,7 @@ superuser = false const originalExit = process.exit; let exitCode: number | undefined; + process.exit = jest.fn((code?: number) => { exitCode = code; throw new Error(`process.exit called with code ${code}`); @@ -232,6 +249,7 @@ superuser = false it('verifies SQL files do not contain transaction statements', async () => { const moduleDir = path.join(fixture.tempDir, 'test-module-no-transactions'); + await setupModule(moduleDir); await runAddTest( diff --git a/packages/cli/__tests__/cli-deploy-fixture.test.ts b/packages/cli/__tests__/cli-deploy-fixture.test.ts index d50f7e287..709b6929e 100644 --- a/packages/cli/__tests__/cli-deploy-fixture.test.ts +++ b/packages/cli/__tests__/cli-deploy-fixture.test.ts @@ -26,9 +26,11 @@ describe('CLIDeployTestFixture', () => { expect(testDb.config.database).toBe(testDb.name); const result = await testDb.query('SELECT 1 as test'); + expect(result.rows[0].test).toBe(1); const schemaExists = await testDb.exists('schema', 'pgpm_migrate'); + expect(schemaExists).toBe(true); }); diff --git a/packages/cli/__tests__/codegen.test.ts b/packages/cli/__tests__/codegen.test.ts index e6e32ad67..388ec8df0 100644 --- a/packages/cli/__tests__/codegen.test.ts +++ b/packages/cli/__tests__/codegen.test.ts @@ -1,5 +1,6 @@ -import path from 'path' import type { ParsedArgs } from 'minimist' +import path from 'path' + import codegenCommand from '../src/commands/codegen' jest.mock('@constructive-io/graphql-codegen', () => { @@ -11,6 +12,7 @@ jest.mock('@constructive-io/graphql-codegen', () => { documents: { ...(a?.documents || {}), ...(b?.documents || {}) }, features: { ...(a?.features || {}), ...(b?.features || {}) } }) + return { runCodegen: jest.fn(async () => ({ root: '/tmp/generated', typesFile: '', operationsDir: '', sdkFile: '' })), defaultGraphQLCodegenOptions: { input: {}, output: { root: 'graphql/codegen/dist' }, documents: {}, features: { emitTypes: true, emitOperations: true, emitSdk: true } }, @@ -37,13 +39,14 @@ describe('codegen command', () => { it('prints usage and exits with code 0 when --help is set', async () => { const spyLog = jest.spyOn(console, 'log').mockImplementation(() => {}) - const spyExit = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error('exit:' + code) }) as any) + const spyExit = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error(`exit:${ code}`) }) as any) const argv: Partial = { help: true } await expect(codegenCommand(argv, {} as any, {} as any)).rejects.toThrow('exit:0') expect(spyLog).toHaveBeenCalled() const first = (spyLog.mock.calls[0]?.[0] as string) || '' + expect(first).toContain('Constructive GraphQL Codegen') spyLog.mockRestore() @@ -106,6 +109,7 @@ describe('codegen command', () => { output: { root: 'custom-root' }, features: { emitTypes: true, emitOperations: true, emitSdk: false } } + ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(cfg)) const argv: Partial = { @@ -119,6 +123,7 @@ describe('codegen command', () => { expect(runCodegen).toHaveBeenCalled() const call = (runCodegen as jest.Mock).mock.calls[0] const options = call[0] + expect(options.input.schema).toBe('from-config.graphql') expect(options.output.root).toBe('graphql/codegen/dist') expect(options.documents.format).toBe('ts') diff --git a/packages/cli/__tests__/deploy.test.ts b/packages/cli/__tests__/deploy.test.ts index 8966072c6..95efa42ea 100644 --- a/packages/cli/__tests__/deploy.test.ts +++ b/packages/cli/__tests__/deploy.test.ts @@ -34,6 +34,7 @@ describe('CLI Deploy Command', () => { expect(await testDb.exists('schema', 'myfirstapp')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.find((change: any) => change.package === 'my-first' && change.change_name === 'schema_myfirstapp')).toBeTruthy(); expect(deployedChanges.find((change: any) => change.package === 'my-second')).toBeFalsy(); expect(deployedChanges.find((change: any) => change.package === 'my-third')).toBeFalsy(); @@ -51,6 +52,7 @@ describe('CLI Deploy Command', () => { expect(await testDb.exists('table', 'myfirstapp.products')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true); expect(deployedChanges.find((change: any) => change.package === 'my-second')).toBeFalsy(); expect(deployedChanges.find((change: any) => change.package === 'my-third')).toBeFalsy(); @@ -76,6 +78,7 @@ describe('CLI Deploy Command', () => { expect(await testDb.exists('table', 'mythirdapp.customers')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true); expect(deployedChanges.some((change: any) => change.package === 'my-second')).toBe(true); expect(deployedChanges.some((change: any) => change.package === 'my-third')).toBe(true); @@ -83,6 +86,7 @@ describe('CLI Deploy Command', () => { it('should revert changes via CLI', async () => { const deployCommands = `cnc deploy --database ${testDb.name} --package my-first --yes`; + await fixture.runTerminalCommands(deployCommands, { database: testDb.name }, true); @@ -92,6 +96,7 @@ describe('CLI Deploy Command', () => { expect(await testDb.exists('table', 'myfirstapp.products')).toBe(true); const revertCommands = `cnc revert --database $database --package my-first --yes`; + await fixture.runTerminalCommands(revertCommands, { database: testDb.name }, true); @@ -114,6 +119,7 @@ describe('CLI Deploy Command', () => { expect(await testDb.exists('table', 'myfirstapp.products')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true); expect(deployedChanges.find((change: any) => change.package === 'my-second')).toBeFalsy(); expect(deployedChanges.find((change: any) => change.package === 'my-third')).toBeFalsy(); diff --git a/packages/cli/__tests__/extensions.test.ts b/packages/cli/__tests__/extensions.test.ts index f0036ed5a..19c378867 100644 --- a/packages/cli/__tests__/extensions.test.ts +++ b/packages/cli/__tests__/extensions.test.ts @@ -38,6 +38,7 @@ describe('cmds:extension', () => { }); const isInit = Array.isArray(argv._) && argv._.includes('init'); + if (isInit) { argv = withInitDefaults(argv); } diff --git a/packages/cli/__tests__/get-graphql-schema.test.ts b/packages/cli/__tests__/get-graphql-schema.test.ts index 587ddfa27..fa7ef2296 100644 --- a/packages/cli/__tests__/get-graphql-schema.test.ts +++ b/packages/cli/__tests__/get-graphql-schema.test.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; + import runGetGraphqlSchema from '../src/commands/get-graphql-schema'; jest.mock('@constructive-io/graphql-server', () => ({ @@ -44,6 +45,7 @@ describe('cnc get-graphql-schema (mocked)', () => { expect(fs.existsSync(outDbFile)).toBe(true); const sdl = fs.readFileSync(outDbFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('hello'); }); @@ -59,6 +61,7 @@ describe('cnc get-graphql-schema (mocked)', () => { expect(fs.existsSync(outEndpointFile)).toBe(true); const sdl = fs.readFileSync(outEndpointFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('greeting'); }); @@ -76,13 +79,16 @@ describe('cnc get-graphql-schema (mocked)', () => { // Verify file written expect(fs.existsSync(outEndpointHeaderHostFile)).toBe(true); const sdl = fs.readFileSync(outEndpointHeaderHostFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('greeting'); // Verify the mocked function received the headerHost argument const server = jest.requireMock('@constructive-io/graphql-server') as any; + expect(server.fetchEndpointSchemaSDL).toHaveBeenCalled(); const lastCall = server.fetchEndpointSchemaSDL.mock.calls[server.fetchEndpointSchemaSDL.mock.calls.length - 1]; + expect(lastCall[0]).toBe('http://localhost:5555/graphql'); expect(lastCall[1]).toEqual({ headerHost: 'meta8.localhost' }); }); @@ -100,13 +106,16 @@ describe('cnc get-graphql-schema (mocked)', () => { // Verify file written expect(fs.existsSync(outEndpointAuthFile)).toBe(true); const sdl = fs.readFileSync(outEndpointAuthFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('greeting'); // Verify the mocked function received the auth argument const server = jest.requireMock('@constructive-io/graphql-server') as any; + expect(server.fetchEndpointSchemaSDL).toHaveBeenCalled(); const lastCall = server.fetchEndpointSchemaSDL.mock.calls[server.fetchEndpointSchemaSDL.mock.calls.length - 1]; + expect(lastCall[0]).toBe('http://localhost:5555/graphql'); expect(lastCall[1]).toEqual({ auth: 'Bearer 123' }); }); @@ -124,13 +133,16 @@ describe('cnc get-graphql-schema (mocked)', () => { // Verify file written expect(fs.existsSync(outEndpointHeadersFile)).toBe(true); const sdl = fs.readFileSync(outEndpointHeadersFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('greeting'); // Verify the mocked function received the headers argument const server = jest.requireMock('@constructive-io/graphql-server') as any; + expect(server.fetchEndpointSchemaSDL).toHaveBeenCalled(); const lastCall = server.fetchEndpointSchemaSDL.mock.calls[server.fetchEndpointSchemaSDL.mock.calls.length - 1]; + expect(lastCall[0]).toBe('http://localhost:5555/graphql'); expect(lastCall[1]).toEqual({ headers: { 'X-Mode': 'fast', Authorization: 'Bearer ABC' } }); }); @@ -149,13 +161,16 @@ describe('cnc get-graphql-schema (mocked)', () => { // Verify file written expect(fs.existsSync(outEndpointAuthAndHeaderFile)).toBe(true); const sdl = fs.readFileSync(outEndpointAuthAndHeaderFile, 'utf8'); + expect(sdl).toContain('type Query'); expect(sdl).toContain('greeting'); // Verify the mocked function received both auth and headers const server = jest.requireMock('@constructive-io/graphql-server') as any; + expect(server.fetchEndpointSchemaSDL).toHaveBeenCalled(); const lastCall = server.fetchEndpointSchemaSDL.mock.calls[server.fetchEndpointSchemaSDL.mock.calls.length - 1]; + expect(lastCall[0]).toBe('http://localhost:5555/graphql'); expect(lastCall[1]).toEqual({ auth: 'Bearer 123', headers: { Authorization: 'Bearer override', 'X-Mode': 'fast' } }); }); diff --git a/packages/cli/__tests__/init.install.test.ts b/packages/cli/__tests__/init.install.test.ts index c4957e39b..e5ca87c72 100644 --- a/packages/cli/__tests__/init.install.test.ts +++ b/packages/cli/__tests__/init.install.test.ts @@ -18,6 +18,7 @@ describe('cmds:install - with initialized workspace and module', () => { const workspaceName = 'my-workspace'; const moduleName = 'my-module'; + workspaceDir = path.join(fixture.tempDir, workspaceName); moduleDir = path.join(workspaceDir, 'packages', moduleName); @@ -34,7 +35,7 @@ describe('cmds:install - with initialized workspace and module', () => { _: ['init'], cwd: workspaceDir, name: moduleName, - moduleName: moduleName, + moduleName, extensions: ['uuid-ossp', 'plpgsql'], }); }); @@ -55,6 +56,7 @@ describe('cmds:install - with initialized workspace and module', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson).toMatchSnapshot(); const installedFiles = glob.sync('**/*', { @@ -73,6 +75,7 @@ describe('cmds:install - with initialized workspace and module', () => { // Snapshot control file const mod = new PgpmPackage(moduleDir); const controlFile = mod.getModuleControlFile(); + expect(controlFile).toMatchSnapshot(); }); @@ -99,6 +102,7 @@ describe('cmds:install - with initialized workspace and module', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson).toMatchSnapshot(); const extPath = path.join(workspaceDir, 'extensions'); @@ -119,6 +123,7 @@ describe('cmds:install - with initialized workspace and module', () => { // Snapshot control file after both installs const mod = new PgpmPackage(moduleDir); const controlFile = mod.getModuleControlFile(); + expect(controlFile).toMatchSnapshot(); }); }); diff --git a/packages/cli/__tests__/init.templates.test.ts b/packages/cli/__tests__/init.templates.test.ts index 237ef63ae..8c02d5f3d 100644 --- a/packages/cli/__tests__/init.templates.test.ts +++ b/packages/cli/__tests__/init.templates.test.ts @@ -1,11 +1,10 @@ jest.setTimeout(30000); +import { scaffoldTemplate } from '@pgpmjs/core'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { scaffoldTemplate } from '@pgpmjs/core'; - const TEMPLATE_REPO = 'https://github.com/constructive-io/pgpm-boilerplates.git'; describe('Template scaffolding', () => { diff --git a/packages/cli/__tests__/init.test.ts b/packages/cli/__tests__/init.test.ts index e3ab46a55..a65308d68 100644 --- a/packages/cli/__tests__/init.test.ts +++ b/packages/cli/__tests__/init.test.ts @@ -106,6 +106,7 @@ describe('cmds:init', () => { ); const cnc = new PgpmPackage(moduleDir); + expect(cnc.getModuleControlFile()).toMatchSnapshot(); }); @@ -135,6 +136,7 @@ describe('cmds:init', () => { }); const workspaceDir = path.join(fixture.tempDir, 'test-workspace-template'); + expect(existsSync(workspaceDir)).toBe(true); expect(existsSync(path.join(workspaceDir, 'package.json'))).toBe(true); expect(existsSync(path.join(workspaceDir, 'pgpm.json'))).toBe(true); @@ -181,6 +183,7 @@ describe('cmds:init', () => { }); const moduleDir = path.join(workspaceDir, 'packages', 'test-module-template'); + expect(existsSync(moduleDir)).toBe(true); expect(existsSync(path.join(moduleDir, 'package.json'))).toBe(true); expect(existsSync(path.join(moduleDir, 'pgpm.plan'))).toBe(true); @@ -215,6 +218,7 @@ describe('cmds:init', () => { }); const workspaceDir = path.join(fixture.tempDir, 'test-workspace-repo'); + expect(existsSync(workspaceDir)).toBe(true); expect(existsSync(path.join(workspaceDir, 'package.json'))).toBe(true); }, @@ -248,6 +252,7 @@ describe('cmds:init', () => { }); const workspaceDir = path.join(fixture.tempDir, 'test-workspace-branch'); + expect(existsSync(workspaceDir)).toBe(true); expect(existsSync(path.join(workspaceDir, 'package.json'))).toBe(true); }, @@ -298,6 +303,7 @@ describe('cmds:init', () => { }); const modDir = path.join(packagesDir, modName); + expect(existsSync(modDir)).toBe(true); expect(existsSync(path.join(modDir, 'pgpm.plan'))).toBe(true); expect(existsSync(path.join(modDir, 'package.json'))).toBe(true); @@ -328,6 +334,7 @@ describe('cmds:init', () => { }); const firstMod = 'first-mod'; + await commands(withInitDefaults({ _: ['init'], cwd: wsRoot, @@ -360,6 +367,7 @@ describe('cmds:init', () => { }); const secondModDir = path.join(packagesDir, secondMod); + expect(existsSync(secondModDir)).toBe(true); expect(existsSync(path.join(secondModDir, 'pgpm.plan'))).toBe(true); expect(existsSync(path.join(secondModDir, 'package.json'))).toBe(true); @@ -392,6 +400,7 @@ describe('cmds:init', () => { }); const baseMod = 'base-mod'; + await commands(withInitDefaults({ _: ['init'], cwd: wsRoot, @@ -431,9 +440,11 @@ describe('cmds:init', () => { expect(errorSpy).toHaveBeenCalled(); const errorCalls = errorSpy.mock.calls.map(call => call.join(' ')); const hasNestedError = errorCalls.some(call => call.includes('Cannot create a module inside an existing module')); + expect(hasNestedError).toBe(true); const nestedDir = path.join(insideDir, nestedName); + expect(existsSync(nestedDir)).toBe(false); exitSpy.mockRestore(); @@ -467,6 +478,7 @@ describe('cmds:init', () => { }); const modName = 'mod-from-root'; + await commands(withInitDefaults({ _: ['init'], cwd: wsRoot, @@ -482,6 +494,7 @@ describe('cmds:init', () => { }); const modDir = path.join(wsRoot, 'packages', modName); + expect(existsSync(modDir)).toBe(true); expect(existsSync(path.join(modDir, 'pgpm.plan'))).toBe(true); expect(existsSync(path.join(modDir, 'package.json'))).toBe(true); diff --git a/packages/cli/__tests__/package-alias.test.ts b/packages/cli/__tests__/package-alias.test.ts index a31314aae..17c81f1ed 100644 --- a/packages/cli/__tests__/package-alias.test.ts +++ b/packages/cli/__tests__/package-alias.test.ts @@ -1,5 +1,4 @@ import * as fs from 'fs'; -import * as path from 'path'; import { teardownPgPools } from 'pg-cache'; import { CLIDeployTestFixture } from '../test-utils'; @@ -17,6 +16,7 @@ describe('CLI Package Alias Resolution', () => { // This simulates the case where package.json name differs from control file name const packageJsonPath = fixture.fixturePath('packages', 'my-first', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + packageJson.name = '@test-scope/my-first'; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); }); @@ -43,6 +43,7 @@ describe('CLI Package Alias Resolution', () => { // Verify the deployed changes are recorded under the control file name (my-first), not the npm name const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true); }); @@ -58,6 +59,7 @@ describe('CLI Package Alias Resolution', () => { expect(await testDb.exists('schema', 'myfirstapp')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true); }); @@ -73,6 +75,7 @@ describe('CLI Package Alias Resolution', () => { expect(await testDb.exists('schema', 'myfirstapp')).toBe(true); const deployedChanges = await testDb.getDeployedChanges(); + expect(deployedChanges.find((change: any) => change.package === 'my-first' && change.change_name === 'schema_myfirstapp' )).toBeTruthy(); @@ -81,6 +84,7 @@ describe('CLI Package Alias Resolution', () => { it('should revert using npm package name alias', async () => { // First deploy const deployCommands = `cnc deploy --database ${testDb.name} --package @test-scope/my-first --yes`; + await fixture.runTerminalCommands(deployCommands, { database: testDb.name }, true); @@ -89,6 +93,7 @@ describe('CLI Package Alias Resolution', () => { // Then revert using the aliased npm name const revertCommands = `cnc revert --database ${testDb.name} --package @test-scope/my-first --yes`; + await fixture.runTerminalCommands(revertCommands, { database: testDb.name }, true); diff --git a/packages/cli/__tests__/package.test.ts b/packages/cli/__tests__/package.test.ts index 0662b4f34..9347a591c 100644 --- a/packages/cli/__tests__/package.test.ts +++ b/packages/cli/__tests__/package.test.ts @@ -44,6 +44,7 @@ describe('cmds:package', () => { it('updates module with `extension` and `package` commands in copied fixture workspace', async () => { const fixtureWorkspace = fixture('constructive'); const workspacePath = path.join(tempDir, 'constructive'); + fs.cpSync(fixtureWorkspace, workspacePath, { recursive: true }); const modulePath = path.join(workspacePath, 'packages', 'secrets'); @@ -63,6 +64,7 @@ describe('cmds:package', () => { // Remove stale SQL to trigger regeneration const { sqlFile } = initialProject.getModuleInfo(); + fs.rmSync(path.join(modulePath, sqlFile)); // Rebuild SQL package @@ -73,6 +75,7 @@ describe('cmds:package', () => { // Snapshot generated SQL const sql = fs.readFileSync(path.join(modulePath, sqlFile), 'utf-8'); + expect(sql).toMatchSnapshot('extension-update - sql'); // Snapshot resulting file structure diff --git a/packages/cli/__tests__/upgrade-modules.test.ts b/packages/cli/__tests__/upgrade-modules.test.ts index 70bd2a91a..dfa940d76 100644 --- a/packages/cli/__tests__/upgrade-modules.test.ts +++ b/packages/cli/__tests__/upgrade-modules.test.ts @@ -17,6 +17,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { const workspaceName = 'my-workspace'; const moduleName = 'my-module'; + workspaceDir = path.join(fixture.tempDir, workspaceName); moduleDir = path.join(workspaceDir, 'packages', moduleName); @@ -33,7 +34,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { _: ['init'], cwd: workspaceDir, name: moduleName, - moduleName: moduleName, + moduleName, extensions: ['uuid-ossp', 'plpgsql'], }); }); @@ -52,6 +53,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { // Should complete without error const mod = new PgpmPackage(moduleDir); const result = mod.getInstalledModules(); + expect(result.installed).toEqual([]); }); }); @@ -74,6 +76,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.2.0'); }); }); @@ -96,6 +99,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.1.0'); }); @@ -103,6 +107,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { let pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.1.0'); await fixture.runCmd({ @@ -121,6 +126,7 @@ describe('cmds:upgrade-modules - with initialized workspace and module', () => { let pkgJson = JSON.parse( fs.readFileSync(path.join(moduleDir, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.1.0'); await fixture.runCmd({ diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 3b03799be..21035e7d6 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -27,6 +27,7 @@ export const commands = async (argv: Partial, prompter: Inquirerer, // Run update check early so it shows on help/version paths too try { const pkg = findAndRequirePackageJson(__dirname); + await checkForUpdates({ command: command || 'help', pkgName: pkg.name, @@ -41,6 +42,7 @@ export const commands = async (argv: Partial, prompter: Inquirerer, if (argv.version || argv.v) { const pkg = findAndRequirePackageJson(__dirname); + console.log(pkg.version); process.exit(0); } @@ -75,6 +77,7 @@ export const commands = async (argv: Partial, prompter: Inquirerer, options: Object.keys(commandMap) } ]); + command = answer.command; } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index b3fd5a93a..72fb05ee8 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,10 +1,10 @@ +import { defaultGraphQLCodegenOptions, GraphQLCodegenOptions,mergeGraphQLCodegenOptions, runCodegen } from '@constructive-io/graphql-codegen' +import { fetchEndpointSchemaSDL } from '@constructive-io/graphql-server' +import { promises as fs } from 'fs' import { CLIOptions, Inquirerer } from 'inquirerer' +import yaml from 'js-yaml' import { ParsedArgs } from 'minimist' -import { promises as fs } from 'fs' import { join } from 'path' -import yaml from 'js-yaml' -import { runCodegen, defaultGraphQLCodegenOptions, mergeGraphQLCodegenOptions, GraphQLCodegenOptions } from '@constructive-io/graphql-codegen' -import { fetchEndpointSchemaSDL } from '@constructive-io/graphql-server' const usage = ` Constructive GraphQL Codegen: @@ -34,15 +34,19 @@ function parseBool(v: any, d: boolean): boolean { if (v === undefined) return d if (typeof v === 'boolean') return v const s = String(v).toLowerCase() + if (s === 'true') return true if (s === 'false') return false + return d } async function loadConfig(path: string): Promise> { const content = await fs.readFile(path, 'utf8') + if (/\.ya?ml$/i.test(path)) return yaml.load(content) as any if (/\.json$/i.test(path)) return JSON.parse(content) + return {} } @@ -60,9 +64,11 @@ export default async ( const configPath = (argv.config as string) || '' let fileOpts: Partial = {} + if (configPath) fileOpts = await loadConfig(configPath) const overrides: Partial = {} + if (argv.schema) overrides.input = { ...(overrides.input || {}), schema: String(argv.schema) } if (argv.endpoint) overrides.input = { ...(overrides.input || {}), endpoint: String(argv.endpoint) } as any const headerHost = (argv.headerHost as string) ?? '' @@ -70,12 +76,15 @@ export default async ( const headerArg = argv.header as string | string[] | undefined const headerList = Array.isArray(headerArg) ? headerArg : headerArg ? [headerArg] : [] const headers: Record = {} + if (auth) headers['Authorization'] = auth for (const h of headerList) { const idx = typeof h === 'string' ? h.indexOf(':') : -1 + if (idx <= 0) continue const name = h.slice(0, idx).trim() const value = h.slice(idx + 1).trim() + if (!name) continue headers[name] = value } @@ -89,12 +98,14 @@ export default async ( const allowQueries = Array.isArray(allowQueryArg) ? allowQueryArg : allowQueryArg ? [String(allowQueryArg)] : [] const excludeQueries = Array.isArray(excludeQueryArg) ? excludeQueryArg : excludeQueryArg ? [String(excludeQueryArg)] : [] const excludePatterns = Array.isArray(excludePatternArg) ? excludePatternArg : excludePatternArg ? [String(excludePatternArg)] : [] + if (allowQueries.length || excludeQueries.length || excludePatterns.length) { overrides.documents = { ...(overrides.documents || {}), allowQueries, excludeQueries, excludePatterns } as any } const emitTypes = parseBool(argv.emitTypes, true) const emitOperations = parseBool(argv.emitOperations, true) const emitSdk = parseBool(argv.emitSdk, true) + overrides.features = { emitTypes, emitOperations, emitSdk } const merged = mergeGraphQLCodegenOptions(defaultGraphQLCodegenOptions, fileOpts as any) @@ -102,11 +113,13 @@ export default async ( if (finalOptions.input.endpoint && headerHost) { const opts: any = {} + if (headerHost) opts.headerHost = headerHost if (auth) opts.auth = auth if (Object.keys(headers).length) opts.headers = headers const sdl = await (fetchEndpointSchemaSDL as any)(String(finalOptions.input.endpoint), opts) const tmpSchemaPath = join(cwd, '.constructive-codegen-schema.graphql') + await fs.writeFile(tmpSchemaPath, sdl, 'utf8') finalOptions.input.schema = tmpSchemaPath as any ;(finalOptions.input as any).endpoint = '' @@ -114,11 +127,13 @@ export default async ( const hasSchema = !!finalOptions.input.schema && String(finalOptions.input.schema).trim() !== '' const hasEndpoint = !!(finalOptions.input as any).endpoint && String((finalOptions.input as any).endpoint).trim() !== '' + if (!hasSchema && !hasEndpoint) { console.error('Missing --schema or --endpoint or config.input') process.exit(1) } const result = await runCodegen(finalOptions, cwd) + console.log(`Generated at ${join(result.root)}`) } diff --git a/packages/cli/src/commands/get-graphql-schema.ts b/packages/cli/src/commands/get-graphql-schema.ts index eb189f9ca..e1e6e75b5 100644 --- a/packages/cli/src/commands/get-graphql-schema.ts +++ b/packages/cli/src/commands/get-graphql-schema.ts @@ -1,7 +1,7 @@ +import { buildSchemaSDL, fetchEndpointSchemaSDL } from '@constructive-io/graphql-server' +import { promises as fs } from 'fs' import { CLIOptions, Inquirerer } from 'inquirerer' import { ParsedArgs } from 'minimist' -import { promises as fs } from 'fs' -import { buildSchemaSDL, fetchEndpointSchemaSDL } from '@constructive-io/graphql-server' const usage = ` Constructive Get GraphQL Schema: @@ -44,11 +44,14 @@ export default async ( const headerArg = argv.header as string | string[] | undefined const headerList = Array.isArray(headerArg) ? headerArg : headerArg ? [headerArg] : [] const headers: Record = {} + for (const h of headerList) { const idx = typeof h === 'string' ? h.indexOf(':') : -1 + if (idx <= 0) continue const name = h.slice(0, idx).trim() const value = h.slice(idx + 1).trim() + if (!name) continue headers[name] = value } @@ -56,8 +59,10 @@ export default async ( const schemas = schemasArg.split(',').map(s => s.trim()).filter(Boolean) let sdl: string + if (endpoint) { const opts: any = {} + if (headerHost) opts.headerHost = headerHost if (auth) opts.auth = auth if (Object.keys(headers).length) opts.headers = headers @@ -73,6 +78,6 @@ export default async ( await fs.writeFile(out, sdl, 'utf8') console.log(`Wrote schema SDL to ${out}`) } else { - process.stdout.write(sdl + '\n') + process.stdout.write(`${sdl }\n`) } } diff --git a/packages/cli/src/commands/server.ts b/packages/cli/src/commands/server.ts index d2929587c..fb6c13615 100644 --- a/packages/cli/src/commands/server.ts +++ b/packages/cli/src/commands/server.ts @@ -1,6 +1,6 @@ import { getEnvOptions } from '@constructive-io/graphql-env'; -import { Logger } from '@pgpmjs/logger'; import { GraphQLServer as server } from '@constructive-io/graphql-server'; +import { Logger } from '@pgpmjs/logger'; import { PgpmOptions } from '@pgpmjs/types'; import { CLIOptions, Inquirerer, OptionValue,Question } from 'inquirerer'; import { getPgPool } from 'pg-cache'; @@ -130,6 +130,7 @@ export default async ( // Warn when passing CORS override via CLI, especially in production if (origin && origin.trim().length) { const env = (process.env.NODE_ENV || 'development').toLowerCase(); + if (env === 'production') { if (origin.trim() === '*') { log.warn('CORS wildcard ("*") provided via --origin in production: this effectively disables CORS and is not recommended. Prefer per-API CORS via meta schema.'); @@ -142,6 +143,7 @@ export default async ( let selectedSchemas: string[] = []; let authRole: string | undefined; let roleName: string | undefined; + if (!metaApi) { const db = await getPgPool({ database: selectedDb }); const result = await db.query(` @@ -183,6 +185,7 @@ export default async ( required: true } ]); + authRole = selectedAuthRole; roleName = selectedRoleName; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 120f76063..a108c8699 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,6 +6,7 @@ import { commands } from './commands'; if (process.argv.includes('--version') || process.argv.includes('-v')) { const pkg = findAndRequirePackageJson(__dirname); + console.log(pkg.version); process.exit(0); } diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts index a141a6c51..8c0dd24aa 100644 --- a/packages/cli/src/utils/index.ts +++ b/packages/cli/src/utils/index.ts @@ -1,3 +1,3 @@ +export { usageText } from './display'; export { extractFirst } from 'pgpm'; export { cliExitWithError } from 'pgpm'; -export { usageText } from './display'; diff --git a/packages/cli/test-utils/CLIDeployTestFixture.ts b/packages/cli/test-utils/CLIDeployTestFixture.ts index 2d3e46598..a09da0d01 100644 --- a/packages/cli/test-utils/CLIDeployTestFixture.ts +++ b/packages/cli/test-utils/CLIDeployTestFixture.ts @@ -29,6 +29,7 @@ export class CLIDeployTestFixture extends TestFixture { // Create database using admin pool const adminPool = getPgPool(baseConfig); + try { await adminPool.query(`CREATE DATABASE "${dbName}"`); } catch (e) { @@ -57,10 +58,12 @@ export class CLIDeployTestFixture extends TestFixture { // Initialize migrate schema const migrate = new PgpmMigrate(config); + await migrate.initialize(); // Get pool for test database operations const pool = getPgPool(pgConfig); + this.pools.push(pool); const db: TestDatabase = { @@ -80,8 +83,9 @@ export class CLIDeployTestFixture extends TestFixture { ) as exists`, [name] ); + return result.rows[0].exists; - } else { + } const [schema, table] = name.includes('.') ? name.split('.') : ['public', name]; const result = await pool.query( `SELECT EXISTS ( @@ -90,8 +94,9 @@ export class CLIDeployTestFixture extends TestFixture { ) as exists`, [schema, table] ); + return result.rows[0].exists; - } + }, async getDeployedChanges() { @@ -100,6 +105,7 @@ export class CLIDeployTestFixture extends TestFixture { FROM pgpm_migrate.changes ORDER BY deployed_at` ); + return result.rows; }, @@ -138,6 +144,7 @@ export class CLIDeployTestFixture extends TestFixture { WHERE c.package = $1 AND c.change_name = $2`, [packageName, changeName] ); + return result.rows.map((row: any) => row.requires); }, @@ -148,6 +155,7 @@ export class CLIDeployTestFixture extends TestFixture { }; this.databases.push(db); + return db; } @@ -167,6 +175,7 @@ export class CLIDeployTestFixture extends TestFixture { for (const command of commands) { let processedCommand = command; + for (const [key, value] of Object.entries(variables)) { processedCommand = processedCommand.replace(new RegExp(`\\$${key}`, 'g'), value); } @@ -175,6 +184,7 @@ export class CLIDeployTestFixture extends TestFixture { if (tokens[0] === 'cd') { const targetDir = tokens[1]; + if (targetDir.startsWith('/')) { currentDir = targetDir; } else { @@ -184,8 +194,10 @@ export class CLIDeployTestFixture extends TestFixture { } else if (tokens[0] === 'cnc' || tokens[0] === 'constructive') { // Handle Constructive CLI commands const argv = this.parseCliCommand(tokens.slice(1), currentDir); + if (executeCommands) { const result = await this.runCliCommand(argv); + results.push({ command: processedCommand, type: 'cli', result }); } else { results.push({ command: processedCommand, type: 'cli', result: { argv } }); @@ -202,6 +214,7 @@ export class CLIDeployTestFixture extends TestFixture { const re = /("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|--[a-zA-Z0-9_-]+|-[a-zA-Z0-9_]+|[^\s]+)|(\s+)/g; const matches = command.match(re); const tokens = matches ? matches.filter(t => t.trim()) : []; + return tokens; } @@ -212,6 +225,7 @@ export class CLIDeployTestFixture extends TestFixture { }; let i = 0; + while (i < tokens.length) { const token = tokens[i]; @@ -221,6 +235,7 @@ export class CLIDeployTestFixture extends TestFixture { // Handle --no- pattern by setting : false if (key.startsWith('no-')) { const baseKey = key.substring(3); // Remove 'no-' prefix + argv[baseKey] = false; i += 1; } else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) { diff --git a/packages/cli/test-utils/cli.ts b/packages/cli/test-utils/cli.ts index e09059fd4..4b48b8883 100644 --- a/packages/cli/test-utils/cli.ts +++ b/packages/cli/test-utils/cli.ts @@ -1,7 +1,7 @@ +import { cleanAnsi } from 'clean-ansi'; import { CLIOptions } from 'inquirerer'; import readline from 'readline'; import { Readable, Transform, Writable } from 'stream'; -import { cleanAnsi } from 'clean-ansi'; export const KEY_SEQUENCES = { ENTER: '\u000d', @@ -43,6 +43,7 @@ function setupReadlineMock(inputQueue: InputResponse[], currentInputIndex: numbe readline.createInterface = jest.fn().mockReturnValue({ question: (questionText: string, cb: (input: string) => void) => { const nextInput = inputQueue[currentInputIndex++]; + if (nextInput && nextInput.type === 'read') { setTimeout(() => cb(nextInput.value), 1); // simulate readline 1ms } @@ -61,8 +62,8 @@ export function setupTests(): () => TestEnvironment { let writeResults: string[] = []; let transformResults: string[] = []; - let inputQueue: InputResponse[] = []; - let currentInputIndex = 0; + const inputQueue: InputResponse[] = []; + const currentInputIndex = 0; let lastScheduledTime = 0; @@ -80,6 +81,7 @@ export function setupTests(): () => TestEnvironment { const str = chunk.toString(); const humanizedStr = humanizeKeySequences(str); const cleanStr = cleanAnsi(humanizedStr); + writeResults.push(cleanStr); mockWrite(str); callback(); @@ -103,6 +105,7 @@ export function setupTests(): () => TestEnvironment { const data = chunk.toString(); const humanizedData = humanizeKeySequences(data); const cleanData = cleanAnsi(humanizedData); + transformResults.push(cleanData); this.push(chunk); callback(); diff --git a/packages/cli/test-utils/fixtures.ts b/packages/cli/test-utils/fixtures.ts index 3eb7d1e53..c3da70a69 100644 --- a/packages/cli/test-utils/fixtures.ts +++ b/packages/cli/test-utils/fixtures.ts @@ -1,9 +1,9 @@ +import { DEFAULT_TEMPLATE_REPO } from '@pgpmjs/core'; import fs from 'fs'; import { Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; import os from 'os'; import path from 'path'; -import { DEFAULT_TEMPLATE_REPO } from '@pgpmjs/core'; import { commands } from '../src/commands'; import { setupTests, TestEnvironment } from './cli'; @@ -27,6 +27,7 @@ export class TestFixture { if (fixturePath.length > 0) { const originalFixtureDir = getFixturePath(...fixturePath); + this.tempFixtureDir = path.join(this.tempDir, ...fixturePath); cpSync(originalFixtureDir, this.tempFixtureDir, { recursive: true }); } else { diff --git a/packages/cli/test-utils/index.ts b/packages/cli/test-utils/index.ts index 05adba257..f44de4421 100644 --- a/packages/cli/test-utils/index.ts +++ b/packages/cli/test-utils/index.ts @@ -1,5 +1,5 @@ export * from './cli'; export * from './CLIDeployTestFixture'; export * from './fixtures'; -export * from './TestDatabase'; export * from './init-argv'; +export * from './TestDatabase'; diff --git a/packages/cli/test-utils/init-argv.ts b/packages/cli/test-utils/init-argv.ts index b214b6ec2..bfbe36eec 100644 --- a/packages/cli/test-utils/init-argv.ts +++ b/packages/cli/test-utils/init-argv.ts @@ -1,5 +1,5 @@ -import { ParsedArgs } from 'minimist'; import { DEFAULT_TEMPLATE_REPO } from '@pgpmjs/core'; +import { ParsedArgs } from 'minimist'; export const addInitDefaults = (argv: ParsedArgs): ParsedArgs => { const baseName = (argv.moduleName as string) || (argv.name as string) || 'module'; @@ -21,6 +21,7 @@ export const addInitDefaults = (argv: ParsedArgs): ParsedArgs => { export const withInitDefaults = (argv: ParsedArgs, defaultRepo: string = DEFAULT_TEMPLATE_REPO): ParsedArgs => { const args = addInitDefaults(argv); + if (!Array.isArray(args._) || !args._.includes('init')) return args; return { diff --git a/packages/client/__tests__/client.test.ts b/packages/client/__tests__/client.test.ts index c18fd1b7c..1bae6f3ec 100644 --- a/packages/client/__tests__/client.test.ts +++ b/packages/client/__tests__/client.test.ts @@ -18,6 +18,7 @@ it('getClient', async () => { try { await client.withTransaction(async (client: PoolClient) => { const result = await client.query('SELECT 1'); + expect(result.rows[0]['?column?'] || result.rows[0].count).toBe(1); }); } catch (e) { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5288f0c2b..592664830 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -16,6 +16,7 @@ export class Database { */ async withTransaction(fn: (client: PoolClient) => Promise): Promise { const client = await this.pool.connect(); + try { await client.query('BEGIN'); try { diff --git a/packages/csv-to-pg/__tests__/csv2pg.test.ts b/packages/csv-to-pg/__tests__/csv2pg.test.ts index a01136074..64ca16006 100644 --- a/packages/csv-to-pg/__tests__/csv2pg.test.ts +++ b/packages/csv-to-pg/__tests__/csv2pg.test.ts @@ -1,16 +1,17 @@ // @ts-nocheck -import { parse, parseTypes } from '../src'; +import { nodes } from '@pgsql/utils'; +import cases from 'jest-in-case'; import { resolve } from 'path'; import { deparse } from 'pgsql-deparser'; -import { InsertOne, InsertMany } from '../src/utils'; -import cases from 'jest-in-case'; -import { nodes } from '@pgsql/utils'; -const zips = resolve(__dirname + '/../__fixtures__/zip.csv'); -const withHeaders = resolve(__dirname + '/../__fixtures__/headers.csv'); -const withDelimeter = resolve(__dirname + '/../__fixtures__/delimeter.csv'); -const forParse = resolve(__dirname + '/../__fixtures__/parse.csv'); -const testCase = resolve(__dirname + '/../__fixtures__/test-case.csv'); +import { parse, parseTypes } from '../src'; +import { InsertMany,InsertOne } from '../src/utils'; + +const zips = resolve(`${__dirname }/../__fixtures__/zip.csv`); +const withHeaders = resolve(`${__dirname }/../__fixtures__/headers.csv`); +const withDelimeter = resolve(`${__dirname }/../__fixtures__/delimeter.csv`); +const forParse = resolve(`${__dirname }/../__fixtures__/parse.csv`); +const testCase = resolve(`${__dirname }/../__fixtures__/test-case.csv`); it('noop', () => { expect(true).toBe(true); @@ -67,6 +68,7 @@ xdescribe('Insert Many', () => { url, mime: url.endsWith('png') ? 'image/png' : 'image/jpg' }; + return JSON.stringify(obj); } } @@ -142,6 +144,7 @@ xdescribe('Insert Many', () => { types, records }); + expect(deparse([stmt])).toMatchSnapshot(); }); @@ -171,6 +174,7 @@ xdescribe('Insert Many', () => { types, records }); + expect(deparse([stmt])).toMatchSnapshot(); }); diff --git a/packages/csv-to-pg/__tests__/export.test.ts b/packages/csv-to-pg/__tests__/export.test.ts index ab7912d34..b40572c91 100644 --- a/packages/csv-to-pg/__tests__/export.test.ts +++ b/packages/csv-to-pg/__tests__/export.test.ts @@ -1,12 +1,13 @@ // @ts-nocheck -import { parse, parseTypes } from '../src'; import { resolve } from 'path'; import { deparse } from 'pgsql-deparser'; -import { InsertOne, InsertMany } from '../src/utils'; + +import { parse, parseTypes } from '../src'; import { Parser } from '../src/parser'; +import { InsertMany } from '../src/utils'; -const testCase = resolve(__dirname + '/../__fixtures__/test-case.csv'); +const testCase = resolve(`${__dirname }/../__fixtures__/test-case.csv`); const config = { schema: 'collections_public', diff --git a/packages/csv-to-pg/src/cli.ts b/packages/csv-to-pg/src/cli.ts index c7f6c1a80..893ce9463 100644 --- a/packages/csv-to-pg/src/cli.ts +++ b/packages/csv-to-pg/src/cli.ts @@ -1,10 +1,11 @@ #!/usr/bin/env node +import { writeFileSync } from 'fs'; import { CLI, type CommandHandler } from 'inquirerer'; +import { dirname } from 'path'; + import { readConfig } from './parse'; import { Parser } from './parser'; import { normalizePath } from './utils'; -import { dirname } from 'path'; -import { writeFileSync } from 'fs'; interface ConfigFile { input?: string; @@ -22,6 +23,7 @@ interface PromptResult { const handler: CommandHandler = async (argv, prompter) => { // Get config from positional argument or --config flag let configPath = argv._[0] as string | undefined; + if (!configPath && argv.config) { configPath = argv.config as string; } @@ -36,6 +38,7 @@ const handler: CommandHandler = async (argv, prompter) => { required: true } ]); + configPath = result.config; } @@ -77,7 +80,8 @@ const handler: CommandHandler = async (argv, prompter) => { config.output = results.output; let outFile = results.output!; - if (!outFile.endsWith('.sql')) outFile = outFile + '.sql'; + + if (!outFile.endsWith('.sql')) outFile = `${outFile }.sql`; config.output = outFile; if (argv.debug) { @@ -97,4 +101,5 @@ const handler: CommandHandler = async (argv, prompter) => { }; const cli = new CLI(handler, {}); + cli.run(); diff --git a/packages/csv-to-pg/src/parse.ts b/packages/csv-to-pg/src/parse.ts index dbef40690..ad7c9a4f1 100644 --- a/packages/csv-to-pg/src/parse.ts +++ b/packages/csv-to-pg/src/parse.ts @@ -1,12 +1,13 @@ +import type { Node } from '@pgsql/types'; +import { ast, nodes } from '@pgsql/utils'; import csv from 'csv-parser'; import { createReadStream, readFileSync } from 'fs'; import { load as parseYAML } from 'js-yaml'; -import { ast, nodes } from '@pgsql/utils'; -import type { Node } from '@pgsql/types'; + import { + getRelatedField, makeBoundingBox, makeLocation, - getRelatedField, wrapValue } from './utils'; @@ -16,6 +17,7 @@ const NULL_TOKENS = new Set(['NULL', 'null', '\\N', 'NA', 'N/A', 'n/a', '#N/A', function isNumeric(str: unknown): boolean { if (typeof str === 'number') return true; if (typeof str !== 'string') return false; + return ( !isNaN(str as unknown as number) && !isNaN(parseFloat(str)) @@ -30,11 +32,13 @@ const isNullToken = (value: unknown): boolean => { if (typeof value === 'string') { return NULL_TOKENS.has(value.trim()); } + return false; }; const parseJson = (value: unknown): string | undefined => { if (typeof value === 'string') return value; + return value ? JSON.stringify(value) : undefined; }; @@ -49,10 +53,12 @@ const escapeArrayElement = (value: unknown): string => { const str = String(value); // Check if the value needs quoting const needsQuoting = /[,{}"\\]/.test(str) || str.trim() !== str || str === '' || NULL_TOKENS.has(str); + if (needsQuoting) { // Escape backslashes and quotes, then wrap in quotes - return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; + return `"${ str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') }"`; } + return str; }; @@ -63,12 +69,14 @@ const psqlArray = (value: unknown): string | undefined => { if (Array.isArray(value) && value.length) { return `{${value.map(escapeArrayElement).join(',')}}`; } + return undefined; }; export const parse = >(path: string, opts?: csv.Options): Promise => new Promise((resolve, reject) => { const results: T[] = []; + createReadStream(path) .pipe(csv(opts)) .on('data', (data: T) => results.push(data)) @@ -82,6 +90,7 @@ export const parse = >(path: string, opts?: csv.Opti export const readConfig = (config: string): unknown => { let configValue: unknown; + if (config.endsWith('.js')) { configValue = require(config); } else if (config.endsWith('json')) { @@ -91,11 +100,13 @@ export const readConfig = (config: string): unknown => { } else { throw new Error('unsupported config!'); } + return configValue; }; const getFromValue = (from: string | string[]): string[] => { if (Array.isArray(from)) return from; + return [from]; }; @@ -104,27 +115,31 @@ const getFromValue = (from: string | string[]): string[] => { */ const cleanseEmptyStrings = (str: unknown): unknown => { if (isNullToken(str)) return null; + return str; }; const parseBoolean = (str: unknown): boolean | null => { if (typeof str === 'boolean') { return str; - } else if (typeof str === 'string') { + } if (typeof str === 'string') { const s = str.toLowerCase(); + if (s === 'true') { return true; - } else if (s === 't') { + } if (s === 't') { return true; - } else if (s === 'f') { + } if (s === 'f') { return false; - } else if (s === 'false') { + } if (s === 'false') { return false; } - return null; - } else { + return null; } + + return null; + }; const getValuesFromKeys = >(object: T, keys: string[]): unknown[] => @@ -173,6 +188,7 @@ const makeNullOrThrow = (fieldName: string, rawValue: unknown, type: string, req if (required) { throw new ValidationError(fieldName, rawValue, type, reason); } + return nodes.aConst({ isnull: true }); }; @@ -187,6 +203,7 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(rawValue); + if (isEmpty(value) || isNullToken(rawValue)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } @@ -196,12 +213,14 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field const val = nodes.aConst({ ival: ast.integer({ ival: Number(value) }) }); + return wrapValue(val, opts); }; case 'float': return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(rawValue); + if (isEmpty(value) || isNullToken(rawValue)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } @@ -212,6 +231,7 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field const val = nodes.aConst({ fval: ast.float({ fval: String(value) }) }); + return wrapValue(val, opts); }; case 'boolean': @@ -228,17 +248,20 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field const val = nodes.aConst({ boolval: ast.boolean({ boolval: Boolean(value) }) }); + return wrapValue(val, opts); }; case 'bbox': // do bbox magic with args from the fields return (record: Record): Node => { const val = makeBoundingBox(String(parseFn(record[from[0]]))); + return wrapValue(val, opts); }; case 'location': return (record: Record): Node => { const [lon, lat] = getValuesFromKeys(record, from); + if (lon === undefined || lon === null || isNullToken(lon)) { return makeNullOrThrow(fieldName, { lon, lat }, type, required, 'longitude is missing or null'); } @@ -251,6 +274,7 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field // NO parse here... const val = makeLocation(lon as string | number, lat as string | number); + return wrapValue(val, opts); }; case 'related': @@ -272,10 +296,12 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field case 'uuid': return (record: Record): Node => { const rawValue = record[from[0]]; + if (isNullToken(rawValue)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const value = parseFn(rawValue); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty'); } @@ -285,6 +311,7 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field const val = nodes.aConst({ sval: ast.string({ sval: String(value) }) }); + return wrapValue(val, opts); }; case 'timestamp': @@ -292,10 +319,12 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field case 'date': return (record: Record): Node => { const rawValue = record[from[0]]; + if (isNullToken(rawValue)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const value = parseFn(rawValue); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty'); } @@ -308,14 +337,17 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field if (isNaN(dateObj.getTime())) { // Try parsing as epoch timestamp (seconds or milliseconds) const numValue = Number(strValue); + if (!isNaN(numValue)) { // Assume milliseconds if > 10 billion, otherwise seconds const ms = numValue > 10000000000 ? numValue : numValue * 1000; const epochDate = new Date(ms); + if (!isNaN(epochDate.getTime())) { const val = nodes.aConst({ sval: ast.string({ sval: epochDate.toISOString() }) }); + return wrapValue(val, opts); } } @@ -326,18 +358,21 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field const val = nodes.aConst({ sval: ast.string({ sval: type === 'date' ? dateObj.toISOString().split('T')[0] : dateObj.toISOString() }) }); + return wrapValue(val, opts); }; case 'text': return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(cleanseEmptyStrings(rawValue)); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const val = nodes.aConst({ sval: ast.string({ sval: String(value) }) }); + return wrapValue(val, opts); }; @@ -345,12 +380,14 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(psqlArray(cleanseEmptyStrings(rawValue))); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const val = nodes.aConst({ sval: ast.string({ sval: String(value) }) }); + return wrapValue(val, opts); }; case 'image': @@ -360,24 +397,28 @@ const getCoercionFunc = (type: string, from: string[], opts: FieldOptions, field return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(parseJson(cleanseEmptyStrings(rawValue))); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const val = nodes.aConst({ sval: ast.string({ sval: String(value) }) }); + return wrapValue(val, opts); }; default: return (record: Record): Node => { const rawValue = record[from[0]]; const value = parseFn(cleanseEmptyStrings(rawValue)); + if (isEmpty(value)) { return makeNullOrThrow(fieldName, rawValue, type, required, 'value is empty or null'); } const val = nodes.aConst({ sval: ast.string({ sval: String(value) }) }); + return wrapValue(val, opts); }; } @@ -400,11 +441,12 @@ export const parseTypes = (config: Config): TypesMap => { let [key, value] = v; let type: string; let from: string[]; + if (typeof value === 'string') { type = value; from = [key]; if (['related', 'location'].includes(type)) { - throw new Error('must use object for ' + type + ' type'); + throw new Error(`must use object for ${ type } type`); } value = { type, @@ -415,6 +457,7 @@ export const parseTypes = (config: Config): TypesMap => { from = getFromValue(value.from || key); } m[key] = getCoercionFunc(type, from, value, key); + return m; }, {}); }; diff --git a/packages/csv-to-pg/src/parser.ts b/packages/csv-to-pg/src/parser.ts index 1487a42ef..35886576e 100644 --- a/packages/csv-to-pg/src/parser.ts +++ b/packages/csv-to-pg/src/parser.ts @@ -1,7 +1,8 @@ import { readFileSync } from 'fs'; -import { parse, parseTypes } from './index'; import { deparse } from 'pgsql-deparser'; -import { InsertOne, InsertMany } from './utils'; + +import { parse, parseTypes } from './index'; +import { InsertMany,InsertOne } from './utils'; interface ParserConfig { schema?: string; @@ -33,10 +34,12 @@ export class Parser { const { schema, table, singleStmts, conflict, headers, delimeter } = config; const opts: CsvOptions = {}; + if (headers) opts.headers = headers; if (delimeter) opts.separator = delimeter; let records: Record[]; + if (typeof data === 'undefined') { if (config.json || config.input.endsWith('.json')) { records = JSON.parse(readFileSync(config.input, 'utf-8')); @@ -52,6 +55,7 @@ export class Parser { if (config.debug) { console.log(records); + return; } @@ -67,8 +71,9 @@ export class Parser { conflict }) ); + return deparse(stmts); - } else { + } const stmt = InsertMany({ schema, table, @@ -76,7 +81,8 @@ export class Parser { records, conflict }); + return deparse([stmt]); - } + } } diff --git a/packages/csv-to-pg/src/utils.ts b/packages/csv-to-pg/src/utils.ts index 0da82d11e..6ef21b85f 100644 --- a/packages/csv-to-pg/src/utils.ts +++ b/packages/csv-to-pg/src/utils.ts @@ -1,12 +1,13 @@ -import { ast, nodes } from '@pgsql/utils'; import type { Node } from '@pgsql/types'; +import { ast, nodes } from '@pgsql/utils'; import { join, resolve } from 'path'; export const normalizePath = (path: string, cwd?: string): string => - path.startsWith('/') ? path : resolve(join(cwd ? cwd : process.cwd(), path)); + path.startsWith('/') ? path : resolve(join(cwd || process.cwd(), path)); const lstr = (bbox: string): string => { const [lng1, lat1, lng2, lat2] = bbox.split(',').map((a) => a.trim()); + return `LINESTRING(${lng1} ${lat1}, ${lng1} ${lat2}, ${lng2} ${lat2}, ${lng2} ${lat1}, ${lng1} ${lat1})`; }; @@ -25,6 +26,7 @@ export const makeLocation = (longitude: string | number | null | undefined, lati if (longitude === null || longitude === undefined || latitude === null || latitude === undefined) { return nodes.aConst({ isnull: true }); } + return funcCall('st_setsrid', [ funcCall('st_makepoint', [aflt(longitude), aflt(latitude)]), aint(4326) @@ -39,20 +41,24 @@ export const makeBoundingBox = (bbox: string): Node => { } const parts = bbox.split(',').map((a) => a.trim()); + if (parts.length !== 4) { throw new Error(`Invalid bounding box: expected 4 comma-separated values, got ${parts.length}. Value: "${bbox}"`); } const numericParts = parts.map((p, i) => { const num = parseFloat(p); + if (isNaN(num)) { throw new Error(`Invalid bounding box: part ${i + 1} ("${p}") is not a valid number. Value: "${bbox}"`); } + return num; }); // Validate coordinate ranges const [lng1, lat1, lng2, lat2] = numericParts; + if (lng1 < -180 || lng1 > 180 || lng2 < -180 || lng2 > 180) { throw new Error(`Invalid bounding box: longitude must be between -180 and 180. Value: "${bbox}"`); } @@ -133,6 +139,7 @@ import type { OnConflictClause } from '@pgsql/types'; const makeConflictClause = (conflictElems: string[] | undefined, fields: string[]): OnConflictClause | undefined => { if (!conflictElems || !conflictElems.length) return undefined; const setElems = fields.filter((el) => !conflictElems.includes(el)); + if (setElems.length) { return { action: 'ONCONFLICT_UPDATE', @@ -141,14 +148,15 @@ const makeConflictClause = (conflictElems: string[] | undefined, fields: string[ }, targetList: setElems.map((a) => ref(a)) }; - } else { + } + return { action: 'ONCONFLICT_NOTHING', infer: { indexElems: conflictElems.map((a) => indexElem(a)) } }; - } + }; interface InsertOneParams { @@ -256,6 +264,7 @@ export const wrapValue = (val: Node, { wrap, wrapAst, cast }: WrapOptions = {}): } if (wrapAst) return wrapAst(val); if (cast) return makeCast(val, cast); + return val; }; @@ -289,6 +298,7 @@ export const getRelatedField = ({ let val: Node; const value = parse(record[from[0]]); + if (typeof value === 'undefined') { return nodes.aConst({ isnull: true }); } diff --git a/packages/query-builder/__tests__/query-builder.test.ts b/packages/query-builder/__tests__/query-builder.test.ts index c9976bebe..1bf03d275 100644 --- a/packages/query-builder/__tests__/query-builder.test.ts +++ b/packages/query-builder/__tests__/query-builder.test.ts @@ -108,6 +108,7 @@ describe('QueryBuilder', () => { it('should throw an error if no table is specified', () => { const builder = new QueryBuilder(); + expect(() => builder.select(['id']).build()).toThrow( 'Table name or procedure name is not specified.' ); diff --git a/packages/query-builder/src/query-builder.ts b/packages/query-builder/src/query-builder.ts index 138c49d5e..deb4de813 100644 --- a/packages/query-builder/src/query-builder.ts +++ b/packages/query-builder/src/query-builder.ts @@ -26,26 +26,31 @@ export class QueryBuilder { schema(schema: string): this { this.schemaName = schema; + return this; } table(name: string): this { this.entityName = name; + return this; } select(columns: string[] = ['*']): this { this.columns = columns; + return this; } insert(values: Record): this { this.values = values; + return this; } update(values: Record): this { this.values = values; + return this; } @@ -63,12 +68,14 @@ export class QueryBuilder { if (Array.isArray(args)) { // Handle array of arguments const formattedArgs = args.map((arg) => this.formatValue('', arg)).join(', '); + argsClause = args.length > 0 ? `(${formattedArgs})` : '()'; } else if (typeof args === 'object' && args !== null) { // Handle keyed object for named parameters const formattedArgs = Object.entries(args) .map(([key, value]) => `${this.escapeIdentifier(key)} := ${this.formatValue(key, value)}`) .join(', '); + argsClause = Object.keys(args).length > 0 ? `(${formattedArgs})` : '()'; } @@ -81,31 +88,37 @@ export class QueryBuilder { where(column: string, operator: string, value: any): this { this.whereConditions.push({ column, operator, value }); this.parameters.push(value); + return this; } join(type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL', table: string, on: string, schema: string | null = null): this { this.joins.push({ type, schema, table, on }); + return this; } groupBy(columns: string[]): this { this.groupByColumns.push(...columns); + return this; } orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this { this.orderByColumns.push({ column, direction }); + return this; } limit(limit: number): this { this.limitValue = limit; + return this; } setTypes(types: Record): this { this.valueTypes = { ...this.valueTypes, ...types }; + return this; } @@ -184,6 +197,7 @@ export class QueryBuilder { private buildDeleteQuery(): string { const fullyQualifiedTable = this.getFullyQualifiedTable(); const whereClause = this.buildWhereClause(); + return `DELETE FROM ${fullyQualifiedTable}${whereClause};`; } @@ -193,6 +207,7 @@ export class QueryBuilder { const fullyQualifiedJoinTable = schema ? `${this.escapeIdentifier(schema)}.${this.escapeIdentifier(table)}` : this.escapeIdentifier(table); + return ` ${type} JOIN ${fullyQualifiedJoinTable} ON ${on}`; }) .join(''); @@ -218,6 +233,7 @@ export class QueryBuilder { return ''; } const groupBy = this.groupByColumns.map((col) => this.escapeIdentifier(col)).join(', '); + return ` GROUP BY ${groupBy}`; } @@ -228,6 +244,7 @@ export class QueryBuilder { const orderBy = this.orderByColumns .map(({ column, direction }) => `${this.escapeIdentifier(column)} ${direction}`) .join(', '); + return ` ORDER BY ${orderBy}`; } @@ -235,6 +252,7 @@ export class QueryBuilder { if (this.needsQuoting(identifier)) { return `"${identifier.replace(/"/g, '""')}"`; } + return identifier; } @@ -266,13 +284,13 @@ export class QueryBuilder { if (typeof value === 'string') { return `'${value.replace(/'/g, "''")}'`; - } else if (typeof value === 'number' || typeof value === 'boolean') { + } if (typeof value === 'number' || typeof value === 'boolean') { return `${value}`; - } else if (value === null) { + } if (value === null) { return 'NULL'; - } else { + } throw new Error(`Unsupported value type: ${typeof value}`); - } + } private getFullyQualifiedTable(): string { @@ -282,6 +300,7 @@ export class QueryBuilder { if (escapedSchema) { return `${escapedSchema}.${escapedTable}`; } + return escapedTable; } } diff --git a/packages/server-utils/src/cors.ts b/packages/server-utils/src/cors.ts index f6c5e479a..08c3793ec 100644 --- a/packages/server-utils/src/cors.ts +++ b/packages/server-utils/src/cors.ts @@ -30,6 +30,7 @@ export const cors = (app: Express, origin?: string) => { credentials: true, optionsSuccessStatus: 200, }; + return corsPlugin(opts)(req, res, next); }); } diff --git a/packages/server-utils/src/utils.ts b/packages/server-utils/src/utils.ts index a3fac51e5..968724254 100644 --- a/packages/server-utils/src/utils.ts +++ b/packages/server-utils/src/utils.ts @@ -12,6 +12,7 @@ export const poweredBy = (name: string) => { res.set({ 'X-Powered-By': name, }); + return next(); }; }; diff --git a/packages/url-domains/__tests__/domains.test.ts b/packages/url-domains/__tests__/domains.test.ts index a3619ffd2..e90fa1154 100644 --- a/packages/url-domains/__tests__/domains.test.ts +++ b/packages/url-domains/__tests__/domains.test.ts @@ -41,6 +41,7 @@ cases( return options.host; } }; + // @ts-ignore expect(parseReq(req)).toMatchSnapshot(); }, @@ -58,6 +59,7 @@ cases( return options.host; } }; + // @ts-ignore await parseMiddleware()(req, null, () => {}); expect(req).toMatchSnapshot(); diff --git a/packages/url-domains/src/requests.ts b/packages/url-domains/src/requests.ts index 9c5b538a6..a2a04185f 100644 --- a/packages/url-domains/src/requests.ts +++ b/packages/url-domains/src/requests.ts @@ -4,5 +4,6 @@ import { parseUrl } from './urls'; export const parseReq = (req: Request) => { const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; + return parseUrl(fullUrl); }; diff --git a/packages/url-domains/src/urls.ts b/packages/url-domains/src/urls.ts index d1bd0bbe9..d62ef7214 100644 --- a/packages/url-domains/src/urls.ts +++ b/packages/url-domains/src/urls.ts @@ -2,7 +2,7 @@ const ipRegExp = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; export const parseUrl = (input: string | URL) => { const url = typeof input === 'string' ? new URL(input) : input; - let hostname = url.hostname.replace(/^www\./, ''); + const hostname = url.hostname.replace(/^www\./, ''); const parts = hostname.split('.'); let domain: string | null = null; diff --git a/pgpm/cli/src/commands.ts b/pgpm/cli/src/commands.ts index 9be4235a9..bb5f2af80 100644 --- a/pgpm/cli/src/commands.ts +++ b/pgpm/cli/src/commands.ts @@ -19,13 +19,13 @@ import kill from './commands/kill'; import migrate from './commands/migrate'; import _package from './commands/package'; import plan from './commands/plan'; -import updateCmd from './commands/update'; -import upgradeModules from './commands/upgrade-modules'; import remove from './commands/remove'; import renameCmd from './commands/rename'; import revert from './commands/revert'; import tag from './commands/tag'; import testPackages from './commands/test-packages'; +import updateCmd from './commands/update'; +import upgradeModules from './commands/upgrade-modules'; import verify from './commands/verify'; import { extractFirst, usageText } from './utils'; import { cliExitWithError } from './utils/cli-error'; @@ -43,6 +43,7 @@ const withPgTeardown = (fn: Function, skipTeardown: boolean = false) => async (. export const createPgpmCommandMap = (skipPgTeardown: boolean = false): Record => { const pgt = (fn: Function) => withPgTeardown(fn, skipPgTeardown); + return { add, 'admin-users': pgt(adminUsers), @@ -74,6 +75,7 @@ export const createPgpmCommandMap = (skipPgTeardown: boolean = false): Record, prompter: Inquirerer, options: CLIOptions & { skipPgTeardown?: boolean }) => { if (argv.version || argv.v) { const pkg = findAndRequirePackageJson(__dirname); + console.log(pkg.version); process.exit(0); } @@ -101,6 +103,7 @@ export const commands = async (argv: Partial, prompter: Inquirerer, options: Object.keys(commandMap) } ]); + command = answer.command; } diff --git a/pgpm/cli/src/commands/add.ts b/pgpm/cli/src/commands/add.ts index d8eb086b9..9c8d88862 100644 --- a/pgpm/cli/src/commands/add.ts +++ b/pgpm/cli/src/commands/add.ts @@ -51,15 +51,18 @@ export default async ( message: 'Change name', required: true }]) as any; + finalChange = answers.change; } let dependencies: string[] = []; + if (argv.requires) { dependencies = Array.isArray(argv.requires) ? argv.requires : [argv.requires]; } const pkg = new PgpmPackage(path.resolve(cwd)); + pkg.addChange(finalChange, dependencies.length > 0 ? dependencies : undefined, argv.note); return newArgv; diff --git a/pgpm/cli/src/commands/admin-users.ts b/pgpm/cli/src/commands/admin-users.ts index a1894aabe..721293b4e 100644 --- a/pgpm/cli/src/commands/admin-users.ts +++ b/pgpm/cli/src/commands/admin-users.ts @@ -47,6 +47,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: })) } ]); + subcommand = answer.subcommand; } @@ -72,5 +73,6 @@ function getSubcommandDescription(cmd: string): string { add: 'Add database users with roles', remove: 'Remove database users and revoke roles' }; + return descriptions[cmd] || ''; } diff --git a/pgpm/cli/src/commands/admin-users/add.ts b/pgpm/cli/src/commands/admin-users/add.ts index 6b5ec2537..4134cd7fd 100644 --- a/pgpm/cli/src/commands/admin-users/add.ts +++ b/pgpm/cli/src/commands/admin-users/add.ts @@ -60,6 +60,7 @@ export default async ( if (!confirmTest) { log.info('Operation cancelled.'); + return; } @@ -94,6 +95,7 @@ export default async ( if (!yes) { log.info('Operation cancelled.'); + return; } diff --git a/pgpm/cli/src/commands/admin-users/bootstrap.ts b/pgpm/cli/src/commands/admin-users/bootstrap.ts index 59140f62b..b31be34db 100644 --- a/pgpm/cli/src/commands/admin-users/bootstrap.ts +++ b/pgpm/cli/src/commands/admin-users/bootstrap.ts @@ -47,6 +47,7 @@ export default async ( if (!yes) { log.info('Operation cancelled.'); + return; } diff --git a/pgpm/cli/src/commands/admin-users/remove.ts b/pgpm/cli/src/commands/admin-users/remove.ts index 6ca7c50d1..c3baba184 100644 --- a/pgpm/cli/src/commands/admin-users/remove.ts +++ b/pgpm/cli/src/commands/admin-users/remove.ts @@ -60,6 +60,7 @@ export default async ( if (!confirmTest) { log.info('Operation cancelled.'); + return; } @@ -89,6 +90,7 @@ export default async ( if (!yes) { log.info('Operation cancelled.'); + return; } diff --git a/pgpm/cli/src/commands/analyze.ts b/pgpm/cli/src/commands/analyze.ts index 0b66c9fc5..7785240c3 100644 --- a/pgpm/cli/src/commands/analyze.ts +++ b/pgpm/cli/src/commands/analyze.ts @@ -7,13 +7,16 @@ export default async (argv: Partial, _prompter: Inquirerer) => { const cwd = (argv.cwd as string) || process.cwd(); const proj = new PgpmPackage(path.resolve(cwd)); const result = proj.analyzeModule(); + if (result.ok) { console.log(`OK ${result.name}`); + return; } console.log(`NOT OK ${result.name}`); for (const issue of result.issues) { const loc = issue.file ? ` (${issue.file})` : ''; + console.log(`- [${issue.code}] ${issue.message}${loc}`); } }; diff --git a/pgpm/cli/src/commands/cache.ts b/pgpm/cli/src/commands/cache.ts index 7b2972932..278a57d8f 100644 --- a/pgpm/cli/src/commands/cache.ts +++ b/pgpm/cli/src/commands/cache.ts @@ -1,6 +1,7 @@ import { Logger } from '@pgpmjs/logger'; -import { CLIOptions, Inquirerer } from 'inquirerer'; import { CacheManager } from 'create-gen-app'; +import { CLIOptions, Inquirerer } from 'inquirerer'; + import { cliExitWithError } from '../utils/cli-error'; const log = new Logger('cache'); @@ -26,6 +27,7 @@ export default async ( } const action = (argv._?.[0] as string) || 'clean'; + if (action !== 'clean') { console.log(cacheUsageText); await cliExitWithError(`Unknown cache action: ${action}`); diff --git a/pgpm/cli/src/commands/clear.ts b/pgpm/cli/src/commands/clear.ts index e87841c7a..46b33a94e 100644 --- a/pgpm/cli/src/commands/clear.ts +++ b/pgpm/cli/src/commands/clear.ts @@ -30,10 +30,11 @@ export default async ( } ]; - let { yes, cwd } = await prompter.prompt(argv, questions); + const { yes, cwd } = await prompter.prompt(argv, questions); if (!yes) { log.info('Operation cancelled.'); + return; } @@ -46,6 +47,7 @@ export default async ( } const modulePath = pkg.getModulePath(); + if (!modulePath) { throw new Error('Could not resolve module path'); } @@ -61,10 +63,12 @@ export default async ( if (plan.changes.length === 0) { log.info('Plan is already empty - nothing to clear.'); + return; } const firstChange = plan.changes[0].name; + log.info(`Found ${plan.changes.length} changes in plan. Clearing from first change: ${firstChange}`); const opts = getEnvOptions({ diff --git a/pgpm/cli/src/commands/deploy.ts b/pgpm/cli/src/commands/deploy.ts index 286920f7d..a372328ce 100644 --- a/pgpm/cli/src/commands/deploy.ts +++ b/pgpm/cli/src/commands/deploy.ts @@ -112,10 +112,11 @@ export default async ( } ]; - let { yes, recursive, createdb, cwd, tx, fast, logOnly } = await prompter.prompt(argv, questions); + const { yes, recursive, createdb, cwd, tx, fast, logOnly } = await prompter.prompt(argv, questions); if (!yes) { log.info('Operation cancelled.'); + return; } @@ -129,6 +130,7 @@ export default async ( } let packageName: string | undefined; + if (recursive) { packageName = await selectPackage(argv, prompter, cwd, 'deploy', log); } @@ -149,12 +151,14 @@ export default async ( const project = new PgpmPackage(cwd); let target: string | undefined; + if (packageName && argv.to) { target = `${packageName}:${argv.to}`; } else if (packageName) { target = packageName; } else if (argv.package && argv.to) { const resolvedPackage = resolvePackageAlias(argv.package as string, cwd); + target = `${resolvedPackage}:${argv.to}`; } else if (argv.package) { target = resolvePackageAlias(argv.package as string, cwd); diff --git a/pgpm/cli/src/commands/docker.ts b/pgpm/cli/src/commands/docker.ts index 5a9e9ef6a..c9028dacf 100644 --- a/pgpm/cli/src/commands/docker.ts +++ b/pgpm/cli/src/commands/docker.ts @@ -80,6 +80,7 @@ function run(command: string, args: string[], options: { stdio?: 'inherit' | 'pi async function checkDockerAvailable(): Promise { try { const result = await run('docker', ['--version']); + return result.code === 0; } catch (error) { return false; @@ -89,9 +90,11 @@ async function checkDockerAvailable(): Promise { async function isContainerRunning(name: string): Promise { try { const result = await run('docker', ['inspect', '-f', '{{.State.Running}}', name]); + if (result.code === 0) { return result.stdout.trim() === 'true'; } + return null; } catch (error) { return null; @@ -101,6 +104,7 @@ async function isContainerRunning(name: string): Promise { async function containerExists(name: string): Promise { try { const result = await run('docker', ['inspect', name]); + return result.code === 0; } catch (error) { return false; @@ -111,8 +115,10 @@ async function startContainer(options: DockerRunOptions): Promise { const { name, image, port, user, password, recreate } = options; const dockerAvailable = await checkDockerAvailable(); + if (!dockerAvailable) { await cliExitWithError('Docker is not installed or not available in PATH. Please install Docker first.'); + return; } @@ -121,14 +127,17 @@ async function startContainer(options: DockerRunOptions): Promise { if (running === true) { console.log(`βœ… Container "${name}" is already running`); + return; } if (recreate && exists) { console.log(`πŸ—‘οΈ Removing existing container "${name}"...`); const removeResult = await run('docker', ['rm', '-f', name], { stdio: 'inherit' }); + if (removeResult.code !== 0) { await cliExitWithError(`Failed to remove container "${name}"`); + return; } } @@ -136,11 +145,13 @@ async function startContainer(options: DockerRunOptions): Promise { if (exists && running === false) { console.log(`πŸ”„ Starting existing container "${name}"...`); const startResult = await run('docker', ['start', name], { stdio: 'inherit' }); + if (startResult.code === 0) { console.log(`βœ… Container "${name}" started successfully`); } else { await cliExitWithError(`Failed to start container "${name}"`); } + return; } @@ -156,6 +167,7 @@ async function startContainer(options: DockerRunOptions): Promise { ]; const runResult = await run('docker', runArgs, { stdio: 'inherit' }); + if (runResult.code === 0) { console.log(`βœ… Container "${name}" created and started successfully`); console.log(`πŸ“Œ PostgreSQL is available at localhost:${port}`); @@ -168,25 +180,32 @@ async function startContainer(options: DockerRunOptions): Promise { async function stopContainer(name: string): Promise { const dockerAvailable = await checkDockerAvailable(); + if (!dockerAvailable) { await cliExitWithError('Docker is not installed or not available in PATH. Please install Docker first.'); + return; } const exists = await containerExists(name); + if (!exists) { console.log(`ℹ️ Container "${name}" not found`); + return; } const running = await isContainerRunning(name); + if (running === false) { console.log(`ℹ️ Container "${name}" is already stopped`); + return; } console.log(`πŸ›‘ Stopping container "${name}"...`); const stopResult = await run('docker', ['stop', name], { stdio: 'inherit' }); + if (stopResult.code === 0) { console.log(`βœ… Container "${name}" stopped successfully`); } else { @@ -210,6 +229,7 @@ export default async ( if (!subcommand) { console.log(dockerUsageText); await cliExitWithError('No subcommand provided. Use "start" or "stop".'); + return; } diff --git a/pgpm/cli/src/commands/env.ts b/pgpm/cli/src/commands/env.ts index 52c61cf9a..3348ed1cb 100644 --- a/pgpm/cli/src/commands/env.ts +++ b/pgpm/cli/src/commands/env.ts @@ -57,8 +57,10 @@ function configToEnvVars(config: PgConfig): Record { function printExports(config: PgConfig): void { const envVars = configToEnvVars(config); + for (const [key, value] of Object.entries(envVars)) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + console.log(`export ${key}="${escapedValue}"`); } } @@ -102,6 +104,7 @@ export default async ( const rawArgs = process.argv.slice(2); let envIndex = rawArgs.findIndex(arg => arg === 'env'); + if (envIndex === -1) { envIndex = 0; } @@ -111,6 +114,7 @@ export default async ( const supabaseIndex = argsAfterEnv.findIndex(arg => arg === '--supabase'); let commandArgs: string[]; + if (supabaseIndex !== -1) { commandArgs = argsAfterEnv.slice(supabaseIndex + 1); } else { @@ -120,12 +124,14 @@ export default async ( commandArgs = commandArgs.filter(arg => arg !== '--cwd' && !arg.startsWith('--cwd=')); const cwdIndex = commandArgs.findIndex(arg => arg === '--cwd'); + if (cwdIndex !== -1 && cwdIndex + 1 < commandArgs.length) { commandArgs.splice(cwdIndex, 2); } if (commandArgs.length === 0) { printExports(profile); + return; } @@ -133,6 +139,7 @@ export default async ( try { const exitCode = await executeCommand(profile, command, args); + process.exit(exitCode); } catch (error) { if (error instanceof Error) { diff --git a/pgpm/cli/src/commands/export.ts b/pgpm/cli/src/commands/export.ts index 416a40f8b..8ab759833 100644 --- a/pgpm/cli/src/commands/export.ts +++ b/pgpm/cli/src/commands/export.ts @@ -53,6 +53,7 @@ export default async ( `); let databases: OptionValue[]; + ({ databases } = await prompter.prompt(argv, [ { type: 'checkbox', @@ -73,6 +74,7 @@ export default async ( `); let database_ids: OptionValue[]; + ({ database_ids } = await prompter.prompt({} as any, [ { type: 'checkbox', diff --git a/pgpm/cli/src/commands/init/index.ts b/pgpm/cli/src/commands/init/index.ts index bdd554dda..60a83b614 100644 --- a/pgpm/cli/src/commands/init/index.ts +++ b/pgpm/cli/src/commands/init/index.ts @@ -51,6 +51,7 @@ async function handlePromptFlow(argv: Partial>, prompter: In ...argv, _: (argv._ ?? []).slice(1) }; + return runWorkspaceSetup(nextArgv, prompter); } diff --git a/pgpm/cli/src/commands/init/module.ts b/pgpm/cli/src/commands/init/module.ts index a707530e6..f3c14992b 100644 --- a/pgpm/cli/src/commands/init/module.ts +++ b/pgpm/cli/src/commands/init/module.ts @@ -44,6 +44,7 @@ export default async function runModuleSetup( const answers = await prompter.prompt(argv, moduleQuestions); const modName = sluggify(answers.moduleName); + // Avoid overlapping readline listeners with create-gen-app's prompts. prompter.close(); @@ -75,5 +76,6 @@ export default async function runModuleSetup( }); log.success(`Initialized module: ${modName}`); + return { ...argv, ...answers }; } diff --git a/pgpm/cli/src/commands/init/workspace.ts b/pgpm/cli/src/commands/init/workspace.ts index 727097cb2..f93e4a3af 100644 --- a/pgpm/cli/src/commands/init/workspace.ts +++ b/pgpm/cli/src/commands/init/workspace.ts @@ -21,6 +21,7 @@ export default async function runWorkspaceSetup( const answers = await prompter.prompt(argv, workspaceQuestions); const { cwd = process.cwd() } = argv; const targetPath = path.join(cwd, sluggify(answers.name)); + // Prevent double-echoed keystrokes by closing our prompter before template prompts. prompter.close(); @@ -31,6 +32,7 @@ export default async function runWorkspaceSetup( // Register workspace.dirname resolver so boilerplate templates can use it via defaultFrom/setFrom // This provides the intended workspace directory name before the folder is created const dirName = path.basename(targetPath); + registerDefaultResolver('workspace.dirname', () => dirName); const scaffoldResult = await scaffoldTemplate({ @@ -52,6 +54,7 @@ export default async function runWorkspaceSetup( const cacheMessage = scaffoldResult.cacheUsed ? `Using cached templates from ${scaffoldResult.templateDir}` : `Fetched templates into ${scaffoldResult.templateDir}`; + log.success(cacheMessage); log.success('Workspace templates rendered.'); diff --git a/pgpm/cli/src/commands/kill.ts b/pgpm/cli/src/commands/kill.ts index 5fcdb4487..6ca11bbac 100644 --- a/pgpm/cli/src/commands/kill.ts +++ b/pgpm/cli/src/commands/kill.ts @@ -51,6 +51,7 @@ export default async ( if (!databasesResult.rows.length) { log.info(`ℹ️ No databases found matching pattern "${argv.pattern}". Exiting.`); + return; } @@ -66,10 +67,12 @@ export default async ( if (!databasesResult.rows.length) { log.info('ℹ️ No databases found to process. Exiting.'); + return; } let databases: OptionValue[]; + ({ databases } = await prompter.prompt(argv, [ { type: 'checkbox', @@ -97,6 +100,7 @@ export default async ( if (!yes) { log.info('❌ Aborted. No actions were taken.'); + return; } diff --git a/pgpm/cli/src/commands/migrate.ts b/pgpm/cli/src/commands/migrate.ts index e57790316..594b882a1 100644 --- a/pgpm/cli/src/commands/migrate.ts +++ b/pgpm/cli/src/commands/migrate.ts @@ -51,6 +51,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: })) } ]); + subcommand = answer.subcommand; } @@ -72,5 +73,6 @@ function getSubcommandDescription(cmd: string): string { list: 'List all changes (deployed and pending)', deps: 'Show change dependencies' }; + return descriptions[cmd] || ''; } diff --git a/pgpm/cli/src/commands/migrate/deps.ts b/pgpm/cli/src/commands/migrate/deps.ts index 3366a158d..8e858a2f1 100644 --- a/pgpm/cli/src/commands/migrate/deps.ts +++ b/pgpm/cli/src/commands/migrate/deps.ts @@ -47,6 +47,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: })) } ]); + changeName = answer.change; } @@ -62,6 +63,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: // Show changes with no dependencies first (roots) const roots = allChanges.filter(c => c.dependencies.length === 0); + console.log('πŸ“Œ Root Changes (no dependencies):\n'); roots.forEach(change => { console.log(` β€’ ${change.name}`); @@ -104,6 +106,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: // All dependencies (recursive) const allDeps = getAllDependencies(change.name, allChanges); + if (allDeps.size > 0) { console.log(`\nπŸ“¦ All Dependencies (${allDeps.size} total):`); Array.from(allDeps).forEach(dep => { @@ -113,6 +116,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: // Dependents (what depends on this) const dependents = allChanges.filter(c => c.dependencies.includes(changeName)); + if (dependents.length > 0) { console.log(`\nπŸ“€ Depended on by (${dependents.length} changes):`); dependents.forEach(dep => { @@ -144,10 +148,12 @@ export default async (argv: Partial, prompter: Inquirerer, options: // Check if this change is deployed const isDeployed = deployedMap.has(changeName); + console.log(` This change: ${isDeployed ? 'βœ… Deployed' : '⏳ Not deployed'}`); // Check dependencies const undeployedDeps = Array.from(allDeps).filter(dep => !deployedMap.has(dep)); + if (undeployedDeps.length > 0) { console.log(` ⚠️ Undeployed dependencies: ${undeployedDeps.join(', ')}`); } else if (allDeps.size > 0) { @@ -156,6 +162,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: // Check dependents const deployedDependents = dependents.filter(d => deployedMap.has(d.name)); + if (deployedDependents.length > 0) { console.log(` ⚠️ Deployed dependents: ${deployedDependents.map(d => d.name).join(', ')}`); } @@ -188,9 +195,10 @@ function buildDependencyTree(changes: any[]): Map { function showDependents(changeName: string, tree: Map, indent: string) { const dependents = tree.get(changeName) || []; + dependents.forEach(dep => { console.log(`${indent}└─ ${dep}`); - showDependents(dep, tree, indent + ' '); + showDependents(dep, tree, `${indent } `); }); } @@ -205,6 +213,7 @@ function getAllDependencies(changeName: string, changes: any[]): Set { if (!deps.has(dep)) { deps.add(dep); const depChange = changes.find(c => c.name === dep); + if (depChange) { addDeps(depChange); } @@ -213,5 +222,6 @@ function getAllDependencies(changeName: string, changes: any[]): Set { } addDeps(change); + return deps; } diff --git a/pgpm/cli/src/commands/migrate/init.ts b/pgpm/cli/src/commands/migrate/init.ts index 74bcf12ce..bd4c722a6 100644 --- a/pgpm/cli/src/commands/migrate/init.ts +++ b/pgpm/cli/src/commands/migrate/init.ts @@ -33,6 +33,7 @@ export default async ( if (!yes) { log.info('Operation cancelled.'); + return; } @@ -54,6 +55,7 @@ export default async ( // Check if there's an existing Sqitch deployment to import const hasSquitch = await client.hasSqitchTables(); + if (hasSquitch) { const { importSquitch } = await prompter.prompt(argv, [ { diff --git a/pgpm/cli/src/commands/migrate/list.ts b/pgpm/cli/src/commands/migrate/list.ts index 464d11f55..e011e9e4c 100644 --- a/pgpm/cli/src/commands/migrate/list.ts +++ b/pgpm/cli/src/commands/migrate/list.ts @@ -69,7 +69,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: const deployed = deployedMap.get(change.name); const status = deployed ? 'βœ…' : '⏳'; const deps = change.dependencies.length > 0 ? change.dependencies.join(', ') : '-'; - const depsDisplay = deps.length > 30 ? deps.substring(0, 27) + '...' : deps; + const depsDisplay = deps.length > 30 ? `${deps.substring(0, 27) }...` : deps; console.log(`${status} ${change.name.padEnd(30)} ${depsDisplay}`); }); diff --git a/pgpm/cli/src/commands/migrate/status.ts b/pgpm/cli/src/commands/migrate/status.ts index 781b6e9eb..f3967f6b1 100644 --- a/pgpm/cli/src/commands/migrate/status.ts +++ b/pgpm/cli/src/commands/migrate/status.ts @@ -60,6 +60,7 @@ export default async (argv: Partial, prompter: Inquirerer, options: if (statusResults.length > 0) { const status = statusResults[0]; + console.log(`Package: ${status.package}`); console.log(`Total Deployed: ${status.totalDeployed}`); @@ -76,17 +77,20 @@ export default async (argv: Partial, prompter: Inquirerer, options: // Show recent changes const recentChanges = await targetClient.getRecentChanges(targetDatabase, 5); + if (recentChanges.length > 0) { console.log('\nπŸ“‹ Recent Changes:\n'); recentChanges.forEach((change: any) => { const status = change.deployed_at ? 'βœ…' : '⏳'; const date = change.deployed_at ? new Date(change.deployed_at).toLocaleString() : 'Not deployed'; + console.log(`${status} ${change.change_name.padEnd(30)} ${date}`); }); } // Show pending changes const pendingChanges = await targetClient.getPendingChanges(planPath, targetDatabase); + if (pendingChanges.length > 0) { console.log(`\n⏳ Pending Changes: ${pendingChanges.length}\n`); pendingChanges.slice(0, 5).forEach((change: string) => { diff --git a/pgpm/cli/src/commands/package.ts b/pgpm/cli/src/commands/package.ts index 0b4272cf6..90006f02f 100644 --- a/pgpm/cli/src/commands/package.ts +++ b/pgpm/cli/src/commands/package.ts @@ -54,13 +54,14 @@ export default async ( } ]; - let { cwd, plan, pretty, functionDelimiter } = await prompter.prompt(argv, questions); + const { cwd, plan, pretty, functionDelimiter } = await prompter.prompt(argv, questions); const project = new PgpmPackage(cwd); project.ensureModule(); const info = project.getModuleInfo(); + info.version; await writePackage({ diff --git a/pgpm/cli/src/commands/remove.ts b/pgpm/cli/src/commands/remove.ts index ce60fc61b..6579141d2 100644 --- a/pgpm/cli/src/commands/remove.ts +++ b/pgpm/cli/src/commands/remove.ts @@ -32,10 +32,11 @@ export default async ( } ]; - let { yes, cwd } = await prompter.prompt(argv, questions); + const { yes, cwd } = await prompter.prompt(argv, questions); if (!yes) { log.info('Operation cancelled.'); + return; } diff --git a/pgpm/cli/src/commands/rename.ts b/pgpm/cli/src/commands/rename.ts index 45bb2b1e6..c996c6a69 100644 --- a/pgpm/cli/src/commands/rename.ts +++ b/pgpm/cli/src/commands/rename.ts @@ -8,6 +8,7 @@ import { cliExitWithError } from '../utils/cli-error'; export default async (argv: Partial, _prompter: Inquirerer) => { const cwd = (argv.cwd as string) || process.cwd(); const to = (argv.to as string) || (argv._ && argv._[0] as string); + if (!to) { await cliExitWithError('Missing new name. Use --to or provide as positional argument.'); } @@ -15,6 +16,7 @@ export default async (argv: Partial, _prompter: Inquirerer) => { const syncPkg = !!argv['sync-pkg-name'] || !!argv.syncPkgName; const proj = new PgpmPackage(path.resolve(cwd)); const res = proj.renameModule(to, { dryRun, syncPackageJsonName: syncPkg }); + if (dryRun) { console.log('Dry run'); } diff --git a/pgpm/cli/src/commands/revert.ts b/pgpm/cli/src/commands/revert.ts index 9c7d3e302..a1dc779ee 100644 --- a/pgpm/cli/src/commands/revert.ts +++ b/pgpm/cli/src/commands/revert.ts @@ -73,16 +73,18 @@ export default async ( } ]; - let { yes, recursive, cwd, tx } = await prompter.prompt(argv, questions); + const { yes, recursive, cwd, tx } = await prompter.prompt(argv, questions); if (!yes) { log.info('Operation cancelled.'); + return; } log.debug(`Using current directory: ${cwd}`); let packageName: string | undefined; + if (recursive && argv.to !== true) { packageName = await selectDeployedPackage(database, argv, prompter, log, 'revert', cwd); if (!packageName) { @@ -112,6 +114,7 @@ export default async ( target = packageName; } else if (argv.package && argv.to) { const resolvedPackage = resolvePackageAlias(argv.package as string, cwd); + target = `${resolvedPackage}:${argv.to}`; } else if (argv.package) { target = resolvePackageAlias(argv.package as string, cwd); diff --git a/pgpm/cli/src/commands/tag.ts b/pgpm/cli/src/commands/tag.ts index 61cfa1f8c..c69573216 100644 --- a/pgpm/cli/src/commands/tag.ts +++ b/pgpm/cli/src/commands/tag.ts @@ -96,6 +96,7 @@ export default async ( if (value.includes('@')) { return false; } + return true; } }); @@ -125,6 +126,7 @@ export default async ( if (argv.package || !pkg.isInModule()) { const moduleMap = pkg.getModuleMap(); const module = moduleMap[packageName]; + if (!module) { throw errors.MODULE_NOT_FOUND({ name: packageName }); } @@ -133,10 +135,12 @@ export default async ( const absoluteModulePath = path.resolve(workspacePath, module.path); const originalCwd = process.cwd(); + process.chdir(absoluteModulePath); try { const modulePkg = new PgpmPackage(absoluteModulePath); + modulePkg.addTag(finalTagName.trim(), changeName?.trim() || undefined, comment?.trim() || undefined); log.info(`Successfully added tag '${finalTagName}' to ${changeName || 'latest change'} in package '${packageName}'`); } finally { diff --git a/pgpm/cli/src/commands/test-packages.ts b/pgpm/cli/src/commands/test-packages.ts index d74ed4d04..0d633cead 100644 --- a/pgpm/cli/src/commands/test-packages.ts +++ b/pgpm/cli/src/commands/test-packages.ts @@ -1,11 +1,11 @@ import { PgpmPackage } from '@pgpmjs/core'; import { getEnvOptions } from '@pgpmjs/env'; import { Logger } from '@pgpmjs/logger'; -import path from 'path'; import { CLIOptions, Inquirerer } from 'inquirerer'; import { ParsedArgs } from 'minimist'; -import { getPgEnvOptions } from 'pg-env'; +import path from 'path'; import { getPgPool } from 'pg-cache'; +import { getPgEnvOptions } from 'pg-env'; const log = new Logger('test-packages'); @@ -58,14 +58,17 @@ async function createDatabase(dbname: string, adminDb: string = 'postgres'): Pro await pool.query(`CREATE DATABASE "${safeName}"`); log.debug(`Created database: ${safeName}`); + return true; } catch (error: any) { if (error.code === '42P04') { // Database already exists, that's fine log.debug(`Database ${dbname} already exists`); + return true; } log.error(`Failed to create database ${dbname}: ${error.message}`); + return false; } } @@ -97,7 +100,9 @@ async function checkPostgresConnection(): Promise { try { const pgEnv = getPgEnvOptions({ database: 'postgres' }); const pool = getPgPool(pgEnv); + await pool.query('SELECT 1'); + return true; } catch { return false; @@ -150,6 +155,7 @@ async function testModule( await workspacePkg.deploy(opts, moduleName, true); } catch (error: any) { await dropDatabase(dbname); + return { moduleName, modulePath, @@ -165,6 +171,7 @@ async function testModule( await workspacePkg.verify(opts, moduleName, true); } catch (error: any) { await dropDatabase(dbname); + return { moduleName, modulePath, @@ -179,6 +186,7 @@ async function testModule( await workspacePkg.revert(opts, moduleName, true); } catch (error: any) { await dropDatabase(dbname); + return { moduleName, modulePath, @@ -193,6 +201,7 @@ async function testModule( await workspacePkg.deploy(opts, moduleName, true); } catch (error: any) { await dropDatabase(dbname); + return { moduleName, modulePath, @@ -206,6 +215,7 @@ async function testModule( await dropDatabase(dbname); console.log(`${GREEN}SUCCESS: Module ${moduleName} passed all tests${NC}`); + return { moduleName, modulePath, @@ -214,6 +224,7 @@ async function testModule( } catch (error: any) { // Ensure cleanup on any unexpected error await dropDatabase(dbname); + return { moduleName, modulePath, @@ -241,6 +252,7 @@ export default async ( // Parse excludes let excludes: string[] = []; + if (argv.exclude) { excludes = (argv.exclude as string).split(',').map(e => e.trim()); } @@ -286,9 +298,11 @@ export default async ( // Filter out excluded modules let filteredModules = modules; + if (excludes.length > 0) { filteredModules = modules.filter(mod => { const moduleName = mod.getModuleName(); + return !excludes.includes(moduleName); }); console.log(`Excluding: ${excludes.join(', ')}`); diff --git a/pgpm/cli/src/commands/update.ts b/pgpm/cli/src/commands/update.ts index 258aafde7..bd57e17cb 100644 --- a/pgpm/cli/src/commands/update.ts +++ b/pgpm/cli/src/commands/update.ts @@ -1,9 +1,10 @@ -import { findAndRequirePackageJson } from 'find-and-require-package-json'; import { Logger } from '@pgpmjs/logger'; -import { CLIOptions, Inquirerer } from 'inquirerer'; import { spawn } from 'child_process'; -import { fetchLatestVersion } from '../utils/npm-version'; +import { findAndRequirePackageJson } from 'find-and-require-package-json'; +import { CLIOptions, Inquirerer } from 'inquirerer'; + import { cliExitWithError } from '../utils/cli-error'; +import { fetchLatestVersion } from '../utils/npm-version'; const log = new Logger('update'); @@ -24,6 +25,7 @@ Options: const runNpmInstall = (pkgName: string, registry?: string): Promise => { return new Promise((resolve, reject) => { const args = ['install', '-g', pkgName]; + if (registry) { args.push('--registry', registry); } @@ -60,6 +62,7 @@ export default async ( if (dryRun) { log.info(`[dry-run] ${npmCommand}`); + return argv; } @@ -68,6 +71,7 @@ export default async ( try { await runNpmInstall(pkgName, registry); const latest = await fetchLatestVersion(pkgName); + if (latest) { log.success(`Successfully updated ${pkgName} to version ${latest}.`); } else { diff --git a/pgpm/cli/src/commands/upgrade-modules.ts b/pgpm/cli/src/commands/upgrade-modules.ts index 17debcfd4..6c43a3d76 100644 --- a/pgpm/cli/src/commands/upgrade-modules.ts +++ b/pgpm/cli/src/commands/upgrade-modules.ts @@ -2,6 +2,7 @@ import { PgpmPackage } from '@pgpmjs/core'; import { Logger } from '@pgpmjs/logger'; import { CLIOptions, Inquirerer, OptionValue, Question } from 'inquirerer'; import { ParsedArgs } from 'minimist'; + import { fetchLatestVersion } from '../utils/npm-version'; const log = new Logger('upgrade-modules'); @@ -74,10 +75,12 @@ async function upgradeModulesForProject( } else { log.info('No pgpm modules are installed in this module.'); } + return false; } const prefix = moduleName ? `[${moduleName}] ` : ''; + log.info(`${prefix}Found ${installed.length} installed module(s). Checking for updates...`); const moduleVersions = await fetchModuleVersions(installedVersions); @@ -85,6 +88,7 @@ async function upgradeModulesForProject( if (modulesWithUpdates.length === 0) { log.success(`${prefix}All modules are already up to date.`); + return false; } @@ -96,6 +100,7 @@ async function upgradeModulesForProject( if (dryRun) { log.info(`${prefix}Dry run - no changes made.`); + return true; } @@ -110,6 +115,7 @@ async function upgradeModulesForProject( if (modulesToUpgrade.length === 0) { log.warn(`${prefix}None of the specified modules have updates available.`); + return false; } } else { @@ -140,6 +146,7 @@ async function upgradeModulesForProject( if (modulesToUpgrade.length === 0) { log.info(`${prefix}No modules selected for upgrade.`); + return false; } } @@ -149,6 +156,7 @@ async function upgradeModulesForProject( await project.upgradeModules({ modules: modulesToUpgrade }); log.success(`${prefix}Upgrade complete!`); + return true; } @@ -181,12 +189,14 @@ export default async ( if (modules.length === 0) { log.info('No modules found in the workspace.'); + return; } log.info(`Found ${modules.length} module(s) in the workspace.\n`); let anyUpgraded = false; + for (const moduleProject of modules) { const moduleName = moduleProject.getModuleName(); const upgraded = await upgradeModulesForProject( @@ -198,6 +208,7 @@ export default async ( specificModules, moduleName ); + if (upgraded) { anyUpgraded = true; } diff --git a/pgpm/cli/src/commands/verify.ts b/pgpm/cli/src/commands/verify.ts index 767a83351..1337b0217 100644 --- a/pgpm/cli/src/commands/verify.ts +++ b/pgpm/cli/src/commands/verify.ts @@ -56,11 +56,12 @@ export default async ( }, ]; - let { recursive, cwd } = await prompter.prompt(argv, questions); + const { recursive, cwd } = await prompter.prompt(argv, questions); log.debug(`Using current directory: ${cwd}`); let packageName: string | undefined; + if (recursive && argv.to !== true) { packageName = await selectDeployedPackage(database, argv, prompter, log, 'verify', cwd); if (!packageName) { @@ -87,6 +88,7 @@ export default async ( target = packageName; } else if (argv.package && argv.to) { const resolvedPackage = resolvePackageAlias(argv.package as string, cwd); + target = `${resolvedPackage}:${argv.to}`; } else if (argv.package) { target = resolvePackageAlias(argv.package as string, cwd); diff --git a/pgpm/cli/src/index.ts b/pgpm/cli/src/index.ts index 1dc50fb83..b7a64437d 100644 --- a/pgpm/cli/src/index.ts +++ b/pgpm/cli/src/index.ts @@ -43,6 +43,7 @@ export const options: Partial = { if (require.main === module) { if (process.argv.includes('--version') || process.argv.includes('-v')) { const pkg = findAndRequirePackageJson(__dirname); + console.log(pkg.version); process.exit(0); } diff --git a/pgpm/cli/src/utils/argv.ts b/pgpm/cli/src/utils/argv.ts index 2a2fb40a2..bf8250bf7 100644 --- a/pgpm/cli/src/utils/argv.ts +++ b/pgpm/cli/src/utils/argv.ts @@ -6,5 +6,6 @@ export const extractFirst = (argv: Partial) => { ...argv, _: argv._?.slice(1) ?? [] }; + return { first, newArgv }; }; diff --git a/pgpm/cli/src/utils/database.ts b/pgpm/cli/src/utils/database.ts index c5b45cd15..da5d878d8 100644 --- a/pgpm/cli/src/utils/database.ts +++ b/pgpm/cli/src/utils/database.ts @@ -45,6 +45,7 @@ export async function getAvailableDatabases(options: DatabaseSelectionOptions = query += ` ORDER BY datname`; const result = await db.query(query); + return result.rows.map((row: any) => row.datname); } @@ -111,6 +112,7 @@ export async function getTargetDatabase( } catch (error) { // Fall back to environment database const pgEnv = getPgEnvOptions(); + if (pgEnv.database) { return pgEnv.database; } diff --git a/pgpm/cli/src/utils/deployed-changes.ts b/pgpm/cli/src/utils/deployed-changes.ts index 72974c83a..a1e832f3a 100644 --- a/pgpm/cli/src/utils/deployed-changes.ts +++ b/pgpm/cli/src/utils/deployed-changes.ts @@ -25,6 +25,7 @@ export async function selectDeployedChange( if (packageStatuses.length === 0) { log.warn('No deployed packages found in database'); + return undefined; } @@ -38,6 +39,7 @@ export async function selectDeployedChange( description: `${status.totalDeployed} changes, last: ${status.lastChange}` })) }]); + selectedPackage = (packageAnswer as any).package; } @@ -45,6 +47,7 @@ export async function selectDeployedChange( if (deployedChanges.length === 0) { log.warn(`No deployed changes found for package ${selectedPackage}`); + return undefined; } @@ -83,6 +86,7 @@ export async function selectDeployedPackage( if (packageStatuses.length === 0) { log.warn('No deployed packages found in database'); + return undefined; } diff --git a/pgpm/cli/src/utils/index.ts b/pgpm/cli/src/utils/index.ts index fb956c353..0498c35d8 100644 --- a/pgpm/cli/src/utils/index.ts +++ b/pgpm/cli/src/utils/index.ts @@ -1,8 +1,8 @@ export * from './argv'; -export * from './database'; -export * from './display'; export * from './cli-error'; +export * from './database'; export * from './deployed-changes'; +export * from './display'; export * from './module-utils'; export * from './npm-version'; export * from './package-alias'; diff --git a/pgpm/cli/src/utils/module-utils.ts b/pgpm/cli/src/utils/module-utils.ts index e6eba11d3..9ec4b3ed7 100644 --- a/pgpm/cli/src/utils/module-utils.ts +++ b/pgpm/cli/src/utils/module-utils.ts @@ -24,29 +24,34 @@ export async function selectPackage( // Check if any modules exist if (!moduleNames.length) { const errorMsg = 'No modules found in the specified directory.'; + if (log) { log.error(errorMsg); + return undefined; - } else { + } prompter.close(); throw errors.NOT_FOUND({}, errorMsg); - } + } // If a specific package was provided, validate it if (argv.package) { const inputPackage = argv.package as string; const packageName = resolvePackageAlias(inputPackage, cwd); + if (log) log.info(`Using specified package: ${packageName}`); if (!moduleNames.includes(packageName)) { const errorMsg = `Package '${packageName}' not found. Available packages: ${moduleNames.join(', ')}`; + if (log) { log.error(errorMsg); + return undefined; - } else { + } throw errors.NOT_FOUND({}, errorMsg); - } + } return packageName; @@ -62,5 +67,6 @@ export async function selectPackage( }]); if (log) log.info(`Selected package: ${selectedPackage}`); + return selectedPackage; } diff --git a/pgpm/cli/src/utils/npm-version.ts b/pgpm/cli/src/utils/npm-version.ts index c9bc1169c..42b99ed1e 100644 --- a/pgpm/cli/src/utils/npm-version.ts +++ b/pgpm/cli/src/utils/npm-version.ts @@ -17,6 +17,7 @@ export async function fetchLatestVersion(pkgName: string, timeoutMs = 5000): Pro : (result as any)?.stdout ?? ''; const parsed = parseVersionOutput(stdout); + return parsed ?? null; } catch { return null; @@ -25,10 +26,12 @@ export async function fetchLatestVersion(pkgName: string, timeoutMs = 5000): Pro const parseVersionOutput = (raw: string): string | null => { const trimmed = raw.trim(); + if (!trimmed.length) return null; try { const parsed = JSON.parse(trimmed); + if (typeof parsed === 'string') return parsed; if (Array.isArray(parsed) && parsed.length) { return String(parsed[parsed.length - 1]); diff --git a/pgpm/cli/src/utils/package-alias.ts b/pgpm/cli/src/utils/package-alias.ts index cd877fc86..87d2ceb4b 100644 --- a/pgpm/cli/src/utils/package-alias.ts +++ b/pgpm/cli/src/utils/package-alias.ts @@ -62,6 +62,7 @@ export function resolvePackageAlias(input: string, cwd: string): string { } const aliasMap = buildPackageAliasMap(cwd); + return aliasMap[input] ?? input; } diff --git a/pgpm/cli/src/utils/update-check.ts b/pgpm/cli/src/utils/update-check.ts index a9f05e751..7cbe651e6 100644 --- a/pgpm/cli/src/utils/update-check.ts +++ b/pgpm/cli/src/utils/update-check.ts @@ -1,13 +1,13 @@ -import { findAndRequirePackageJson } from 'find-and-require-package-json'; import { Logger } from '@pgpmjs/logger'; import { - UpdateCheckConfig, UPDATE_CHECK_APPSTASH_KEY, UPDATE_CHECK_TTL_MS, - UPDATE_PACKAGE_NAME -} from '@pgpmjs/types'; + UPDATE_PACKAGE_NAME, + UpdateCheckConfig} from '@pgpmjs/types'; +import { findAndRequirePackageJson } from 'find-and-require-package-json'; + import { compareVersions, fetchLatestVersion } from './npm-version'; -import { readUpdateConfig, shouldCheck, writeUpdateConfig, UpdateConfigOptions } from './update-config'; +import { readUpdateConfig, shouldCheck, UpdateConfigOptions,writeUpdateConfig } from './update-config'; export interface CheckForUpdatesOptions extends UpdateConfigOptions { pkgName?: string; @@ -23,6 +23,7 @@ const shouldSkip = (command?: string): boolean => { if (process.env.PGPM_SKIP_UPDATE_CHECK) return true; if (process.env.CI === 'true') return true; if (command === 'update') return true; + return false; }; @@ -49,6 +50,7 @@ export async function checkForUpdates(options: CheckForUpdatesOptions = {}): Pro if (needsCheck) { const fetched = await fetchLatestVersion(pkgName); + if (fetched) { latestKnownVersion = fetched; } @@ -91,6 +93,7 @@ export async function checkForUpdates(options: CheckForUpdatesOptions = {}): Pro }; } catch (error) { log.debug('Update check skipped due to error:', error); + return null; } } diff --git a/pgpm/cli/src/utils/update-config.ts b/pgpm/cli/src/utils/update-config.ts index affa22779..30e042a1e 100644 --- a/pgpm/cli/src/utils/update-config.ts +++ b/pgpm/cli/src/utils/update-config.ts @@ -1,7 +1,7 @@ +import { UPDATE_CHECK_APPSTASH_KEY,UpdateCheckConfig } from '@pgpmjs/types'; +import { appstash, resolve as resolveAppstash } from 'appstash'; import fs from 'fs'; import path from 'path'; -import { appstash, resolve as resolveAppstash } from 'appstash'; -import { UpdateCheckConfig, UPDATE_CHECK_APPSTASH_KEY } from '@pgpmjs/types'; export interface UpdateConfigOptions { toolName?: string; @@ -19,16 +19,19 @@ const getConfigPath = (options: UpdateConfigOptions = {}) => { }); const configDir = resolveAppstash(dirs, 'config'); + if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } const fileName = `${(options.key ?? UPDATE_CHECK_APPSTASH_KEY).replace(/[^a-z0-9-_]/gi, '_')}.json`; + return path.join(configDir, fileName); }; export const shouldCheck = (now: number, lastCheckedAt: number | undefined, ttlMs: number): boolean => { if (!lastCheckedAt) return true; + return now - lastCheckedAt > ttlMs; }; @@ -42,6 +45,7 @@ export async function readUpdateConfig(options: UpdateConfigOptions = {}): Promi try { const contents = await fs.promises.readFile(configPath, 'utf8'); const parsed = JSON.parse(contents) as UpdateCheckConfig; + return parsed; } catch { // Corrupted config – clear it @@ -50,12 +54,14 @@ export async function readUpdateConfig(options: UpdateConfigOptions = {}): Promi } catch { // ignore } + return null; } } export async function writeUpdateConfig(config: UpdateCheckConfig, options: UpdateConfigOptions = {}): Promise { const configPath = getConfigPath(options); + await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8'); } diff --git a/pgpm/core/__tests__/config-loading.test.ts b/pgpm/core/__tests__/config-loading.test.ts index 9fb06c0b0..3c2388cc5 100644 --- a/pgpm/core/__tests__/config-loading.test.ts +++ b/pgpm/core/__tests__/config-loading.test.ts @@ -81,6 +81,7 @@ module.exports = { it('should have no config when no config file exists', () => { const project = new PgpmPackage(tempDir); + expect(project.config).toBeUndefined(); expect(project.workspacePath).toBeUndefined(); }); diff --git a/pgpm/core/__tests__/core/add-functionality.test.ts b/pgpm/core/__tests__/core/add-functionality.test.ts index fca192318..7d74a72a6 100644 --- a/pgpm/core/__tests__/core/add-functionality.test.ts +++ b/pgpm/core/__tests__/core/add-functionality.test.ts @@ -1,8 +1,9 @@ +import {mkdirSync, readFileSync, writeFileSync } from 'fs'; +import * as fs from 'fs'; +import { basename, join } from 'path'; + import { PgpmPackage } from '../../src/core/class/pgpm'; import { TestFixture } from '../../test-utils'; -import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; -import { basename, join } from 'path'; -import * as fs from 'fs'; describe('Add functionality', () => { let fixture: TestFixture; @@ -17,6 +18,7 @@ describe('Add functionality', () => { const setupModule = (moduleDir: string) => { const moduleName = basename(moduleDir); + mkdirSync(moduleDir, { recursive: true }); mkdirSync(join(moduleDir, 'deploy'), { recursive: true }); mkdirSync(join(moduleDir, 'revert'), { recursive: true }); @@ -27,6 +29,7 @@ describe('Add functionality', () => { %uri=https://github.com/test/${moduleName} `; + writeFileSync(join(moduleDir, 'pgpm.plan'), planContent); const controlContent = `# ${moduleName} extension @@ -35,17 +38,21 @@ default_version = '0.1.0' relocatable = false superuser = false `; + writeFileSync(join(moduleDir, `${moduleName}.control`), controlContent); }; test('addChange creates change in plan file and SQL files', () => { const moduleDir = join(fixture.tempDir, 'test-module'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); + pkg.addChange('organizations'); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('organizations'); expect(fs.existsSync(join(moduleDir, 'deploy', 'organizations.sql'))).toBe(true); @@ -55,6 +62,7 @@ superuser = false test('addChange with dependencies validates and includes them', () => { const moduleDir = join(fixture.tempDir, 'test-module-deps'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -64,19 +72,23 @@ superuser = false pkg.addChange('contacts', ['users'], 'Adds contacts table'); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('users'); expect(planContent).toContain('contacts'); expect(planContent).toContain('Adds contacts table'); const deployContent = readFileSync(join(moduleDir, 'deploy', 'contacts.sql'), 'utf8'); + expect(deployContent).toContain('requires: users'); }); test('addChange with nested path creates directories', () => { const moduleDir = join(fixture.tempDir, 'test-module-nested'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); + pkg.addChange('api/v1/endpoints'); expect(fs.existsSync(join(moduleDir, 'deploy', 'api', 'v1', 'endpoints.sql'))).toBe(true); @@ -84,14 +96,17 @@ superuser = false expect(fs.existsSync(join(moduleDir, 'verify', 'api', 'v1', 'endpoints.sql'))).toBe(true); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('api/v1/endpoints'); }); test('addChange with deeply nested path creates directories', () => { const moduleDir = join(fixture.tempDir, 'test-module-deep-nested'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); + pkg.addChange('schema/myschema/tables/mytable'); expect(fs.existsSync(join(moduleDir, 'deploy', 'schema', 'myschema', 'tables', 'mytable.sql'))).toBe(true); @@ -99,11 +114,13 @@ superuser = false expect(fs.existsSync(join(moduleDir, 'verify', 'schema', 'myschema', 'tables', 'mytable.sql'))).toBe(true); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('schema/myschema/tables/mytable'); }); test('addChange validates change name', () => { const moduleDir = join(fixture.tempDir, 'test-module-validation'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -116,6 +133,7 @@ superuser = false test('addChange prevents duplicate changes', () => { const moduleDir = join(fixture.tempDir, 'test-module-duplicates'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -127,6 +145,7 @@ superuser = false test('addChange validates dependencies exist', () => { const moduleDir = join(fixture.tempDir, 'test-module-dep-validation'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -136,6 +155,7 @@ superuser = false test('addChange requires module context', () => { const nonModuleDir = join(fixture.tempDir, 'not-a-module'); + mkdirSync(nonModuleDir, { recursive: true }); const pkg = new PgpmPackage(nonModuleDir); @@ -145,9 +165,11 @@ superuser = false test('addChange creates SQL files without transaction statements', () => { const moduleDir = join(fixture.tempDir, 'test-module-no-transactions'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); + pkg.addChange('no-transactions'); const deployContent = readFileSync(join(moduleDir, 'deploy', 'no-transactions.sql'), 'utf8'); @@ -169,6 +191,7 @@ superuser = false test('addChange includes proper headers and comments in SQL files', () => { const moduleDir = join(fixture.tempDir, 'test-module-headers'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -195,6 +218,7 @@ superuser = false test('addChange handles multiple dependencies', () => { const moduleDir = join(fixture.tempDir, 'test-module-multi-deps'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -206,17 +230,20 @@ superuser = false pkg.addChange('user-permissions', ['users', 'roles', 'permissions'], 'Links users to permissions'); const deployContent = readFileSync(join(moduleDir, 'deploy', 'user-permissions.sql'), 'utf8'); + expect(deployContent).toContain('-- requires: users'); expect(deployContent).toContain('-- requires: roles'); expect(deployContent).toContain('-- requires: permissions'); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('user-permissions'); expect(planContent).toContain('Links users to permissions'); }); test('addChange allows cross-module dependencies', () => { const moduleDir = join(fixture.tempDir, 'test-module-cross-deps'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -224,15 +251,18 @@ superuser = false pkg.addChange('adoption-history', ['pets:tables/pets'], 'Adds adoption history table'); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('adoption-history'); expect(planContent).toContain('[pets:tables/pets]'); const deployContent = readFileSync(join(moduleDir, 'deploy', 'adoption-history.sql'), 'utf8'); + expect(deployContent).toContain('-- requires: pets:tables/pets'); }); test('addChange validates local dependencies but not cross-module ones', () => { const moduleDir = join(fixture.tempDir, 'test-module-mixed-deps'); + setupModule(moduleDir); const pkg = new PgpmPackage(moduleDir); @@ -244,6 +274,7 @@ superuser = false pkg.addChange('contacts', ['users', 'pets:tables/pets'], 'Mixed local and cross-module deps'); const planContent = readFileSync(join(moduleDir, 'pgpm.plan'), 'utf8'); + expect(planContent).toContain('contacts'); expect(planContent).toContain('[users pets:tables/pets]'); }); diff --git a/pgpm/core/__tests__/core/analyze-module.test.ts b/pgpm/core/__tests__/core/analyze-module.test.ts index 2124aa0a8..f2336c273 100644 --- a/pgpm/core/__tests__/core/analyze-module.test.ts +++ b/pgpm/core/__tests__/core/analyze-module.test.ts @@ -1,8 +1,9 @@ import fs from 'fs'; import path from 'path'; -import { TestFixture } from '../../test-utils'; + import { parsePlanFile } from '../../src/files/plan/parser'; import { writePlanFile } from '../../src/files/plan/writer'; +import { TestFixture } from '../../test-utils'; let fixture: TestFixture; @@ -22,6 +23,7 @@ describe('PgpmPackage.analyzeModule', () => { expect(result.name).toBe('my-first'); expect(result.ok).toBe(false); const codes = result.issues.map(i => i.code); + expect(codes).toContain('missing_sql'); expect(result.issues.find(i => i.code === 'missing_sql')?.file).toContain(`sql/my-first--`); }); @@ -33,10 +35,12 @@ describe('PgpmPackage.analyzeModule', () => { const version = pkgJson.version; const sqlDir = path.join(modPath, 'sql'); const combined = path.join(sqlDir, `my-first--${version}.sql`); + fs.mkdirSync(sqlDir, { recursive: true }); fs.writeFileSync(combined, '-- combined sql'); const result = project.analyzeModule(); + expect(result.ok).toBe(true); expect(result.issues.length).toBe(0); }); @@ -51,6 +55,7 @@ describe('PgpmPackage.analyzeModule', () => { try { const result = project.analyzeModule(); const codes = result.issues.map(i => i.code); + expect(codes).toContain('missing_control'); } finally { if (fs.existsSync(wrongControlPath)) { @@ -64,11 +69,14 @@ describe('PgpmPackage.analyzeModule', () => { const modPath = project.getModulePath()!; const makefilePath = path.join(modPath, 'Makefile'); const original = fs.readFileSync(makefilePath, 'utf8'); + try { const mutated = original.replace(/^EXTENSION\s*=.*$/m, 'EXTENSION = wrongname'); + fs.writeFileSync(makefilePath, mutated); const result = project.analyzeModule(); const codes = result.issues.map(i => i.code); + expect(codes).toContain('makefile_extension_mismatch'); } finally { fs.writeFileSync(makefilePath, original); @@ -80,11 +88,14 @@ describe('PgpmPackage.analyzeModule', () => { const modPath = project.getModulePath()!; const makefilePath = path.join(modPath, 'Makefile'); const original = fs.readFileSync(makefilePath, 'utf8'); + try { const mutated = original.replace(/^DATA\s*=.*$/m, 'DATA = sql/wrong--0.0.0.sql'); + fs.writeFileSync(makefilePath, mutated); const result = project.analyzeModule(); const codes = result.issues.map(i => i.code); + expect(codes).toContain('makefile_data_mismatch'); } finally { fs.writeFileSync(makefilePath, original); @@ -96,13 +107,16 @@ describe('PgpmPackage.analyzeModule', () => { const modPath = project.getModulePath()!; const planPath = path.join(modPath, 'pgpm.plan'); const original = fs.readFileSync(planPath, 'utf8'); + try { const parsed = parsePlanFile(planPath); + if (!parsed.data) throw new Error('Failed to parse plan'); parsed.data.uri = 'different-uri'; writePlanFile(planPath, parsed.data); const result = project.analyzeModule(); const codes = result.issues.map(i => i.code); + expect(codes).toContain('plan_uri_mismatch'); } finally { fs.writeFileSync(planPath, original); @@ -128,6 +142,7 @@ describe('PgpmPackage.analyzeModule', () => { try { const result = project.analyzeModule(); const codes = result.issues.map(i => i.code); + expect(codes).toContain('missing_deploy_dir'); expect(codes).toContain('missing_revert_dir'); expect(codes).toContain('missing_verify_dir'); diff --git a/pgpm/core/__tests__/core/constructive-project.test.ts b/pgpm/core/__tests__/core/constructive-project.test.ts index fea6c4d59..36d908156 100644 --- a/pgpm/core/__tests__/core/constructive-project.test.ts +++ b/pgpm/core/__tests__/core/constructive-project.test.ts @@ -1,4 +1,4 @@ -import { PgpmPackage, PackageContext } from '../../src/core/class/pgpm'; +import { PackageContext,PgpmPackage } from '../../src/core/class/pgpm'; import { TestFixture } from '../../test-utils'; let fixture: TestFixture; @@ -44,6 +44,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const modules = await project.getModules(); + expect(Array.isArray(modules)).toBe(true); expect(modules.length).toBeGreaterThan(0); modules.forEach(mod => { @@ -56,6 +57,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const name = project.getModuleName(); + expect(typeof name).toBe('string'); expect(name.length).toBeGreaterThan(0); }); @@ -65,6 +67,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const info = project.getModuleInfo(); + expect(info.extname).toBeTruthy(); expect(info.version).toMatch(/\d+\.\d+\.\d+/); expect(info.controlFile).toContain('.control'); @@ -76,6 +79,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const deps = project.getRequiredModules(); + expect(Array.isArray(deps)).toBe(true); expect(deps).toContain('plpgsql'); }); @@ -101,6 +105,7 @@ describe('PgpmPackage', () => { const modName = Object.keys(modules)[0]; const { native, modules: deps } = project.getModuleDependencies(modName); + expect(native).toBeDefined(); expect(deps).toBeDefined(); }); @@ -110,6 +115,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const firstCall = project.getModuleInfo(); + project.clearCache(); const secondCall = project.getModuleInfo(); @@ -129,6 +135,7 @@ describe('PgpmPackage', () => { const modules = project.getModuleMap(); const name = Object.keys(modules)[0]; const change = project.getLatestChange(name); + expect(typeof change).toBe('string'); expect(change.length).toBeGreaterThan(0); }); @@ -138,6 +145,7 @@ describe('PgpmPackage', () => { const project = new PgpmPackage(cwd); const name = Object.keys(project.getModuleMap())[0]; const result = await project.getModuleDependencyChanges(name); + expect(result).toHaveProperty('native'); expect(result).toHaveProperty('modules'); expect(Array.isArray(result.modules)).toBe(true); @@ -147,6 +155,7 @@ describe('PgpmPackage', () => { const cwd = fixture.getFixturePath('constructive'); const project = new PgpmPackage(cwd); const modules = project.getAvailableModules(); + expect(Array.isArray(modules)).toBe(true); expect(modules.length).toBeGreaterThan(0); }); diff --git a/pgpm/core/__tests__/core/module-extensions-with-tags.test.ts b/pgpm/core/__tests__/core/module-extensions-with-tags.test.ts index e10722f59..99bf6f4bb 100644 --- a/pgpm/core/__tests__/core/module-extensions-with-tags.test.ts +++ b/pgpm/core/__tests__/core/module-extensions-with-tags.test.ts @@ -12,16 +12,19 @@ afterAll(() => { it('getModuleExtensions for my-first', () => { const project = fixture.getModuleProject(['simple-w-tags'], 'my-first'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); it('getModuleExtensions for my-second', () => { const project = fixture.getModuleProject(['simple-w-tags'], 'my-second'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); it('getModuleExtensions for my-third', () => { const project = fixture.getModuleProject(['simple-w-tags'], 'my-third'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); @@ -47,12 +50,14 @@ describe('generateModulePlan with tags', () => { it('generates plan for my-second with cross-project dependencies', () => { const project = fixture.getModuleProject(['simple-w-tags'], 'my-second'); const plan = project.generateModulePlan({ includePackages: true }); + expect(cleanText(plan)).toMatchSnapshot(); }); it('generates plan for my-third with multiple cross-project dependencies', () => { const project = fixture.getModuleProject(['simple-w-tags'], 'my-third'); const plan = project.generateModulePlan({ includePackages: true }); + expect(cleanText(plan)).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/core/module-extensions.test.ts b/pgpm/core/__tests__/core/module-extensions.test.ts index d8ed8b2d7..1444df13d 100644 --- a/pgpm/core/__tests__/core/module-extensions.test.ts +++ b/pgpm/core/__tests__/core/module-extensions.test.ts @@ -12,16 +12,19 @@ afterAll(() => { it('getModuleExtensions for my-first', () => { const project = fixture.getModuleProject(['simple'], 'my-first'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); it('getModuleExtensions for my-second', () => { const project = fixture.getModuleProject(['simple'], 'my-second'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); it('getModuleExtensions for my-third', () => { const project = fixture.getModuleProject(['simple'], 'my-third'); + expect(project.getModuleExtensions()).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/core/plan-writing.test.ts b/pgpm/core/__tests__/core/plan-writing.test.ts index dbeccddd4..7feca5e66 100644 --- a/pgpm/core/__tests__/core/plan-writing.test.ts +++ b/pgpm/core/__tests__/core/plan-writing.test.ts @@ -13,25 +13,31 @@ afterEach(() => { describe('PgpmPackage.writeModulePlan', () => { it('writes a clean plan to disk for a module (no projects)', async () => { const mod = fixture.getModuleProject(['.'], 'secrets'); + await mod.writeModulePlan({ includePackages: false }); const plan = mod.getModulePlan(); + expect(plan).toMatchSnapshot(); }); it('writes a clean plan to disk for a module (with projects)', async () => { const mod = fixture.getModuleProject(['.'], 'secrets'); + await mod.writeModulePlan({ includePackages: true }); const plan = mod.getModulePlan(); + expect(plan).toMatchSnapshot(); }); it('writes a plan for a dependency-heavy module (totp)', async () => { const mod = fixture.getModuleProject(['.'], 'totp'); + await mod.writeModulePlan({ includePackages: true }); const plan = mod.getModulePlan(); + expect(plan).toContain('%project=totp'); expect(plan).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/core/rename-module.test.ts b/pgpm/core/__tests__/core/rename-module.test.ts index 845a19aca..f5d7770fc 100644 --- a/pgpm/core/__tests__/core/rename-module.test.ts +++ b/pgpm/core/__tests__/core/rename-module.test.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; + import { TestFixture } from '../../test-utils'; let fixture: TestFixture; @@ -31,15 +32,18 @@ describe('PgpmPackage.renameModule', () => { const modPath = project.getModulePath()!; const res = project.renameModule('renamed_mod', { dryRun: false, syncPackageJsonName: false }); + expect(res.changed.length).toBeGreaterThan(0); const newControl = path.join(modPath, 'renamed_mod.control'); + expect(fs.existsSync(newControl)).toBe(true); const analysis = project.analyzeModule(); const mismatchCodes = analysis.issues.filter(i => i.code === 'plan_project_mismatch' || i.code === 'plan_uri_mismatch' || i.code === 'control_filename_mismatch' ); + expect(mismatchCodes.length).toBe(0); }); }); diff --git a/pgpm/core/__tests__/core/target-api.test.ts b/pgpm/core/__tests__/core/target-api.test.ts index c38d44971..620185b2d 100644 --- a/pgpm/core/__tests__/core/target-api.test.ts +++ b/pgpm/core/__tests__/core/target-api.test.ts @@ -16,6 +16,7 @@ describe('PgpmPackage Target API', () => { describe('parseTarget utility function', () => { test('parses project-only target format', () => { const result = parseTarget('secrets'); + expect(result).toEqual({ packageName: 'secrets', toChange: undefined @@ -24,6 +25,7 @@ describe('PgpmPackage Target API', () => { test('parses project:change target format', () => { const result = parseTarget('secrets:procedures/secretfunction'); + expect(result).toEqual({ packageName: 'secrets', toChange: 'procedures/secretfunction' @@ -32,6 +34,7 @@ describe('PgpmPackage Target API', () => { test('parses project:@tag target format', () => { const result = parseTarget('secrets:@v1.0.0'); + expect(result).toEqual({ packageName: 'secrets', toChange: '@v1.0.0' @@ -61,13 +64,16 @@ describe('PgpmPackage Target API', () => { describe('deploy method with target parameter', () => { let project: PgpmPackage; + beforeEach(() => { const fixturePath = fixture.getFixturePath('constructive'); + project = new PgpmPackage(fixturePath); }); test('deploys with project-only target', async () => { const mockDeploy = jest.spyOn(project as any, 'deploy'); + mockDeploy.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets'); expect(recursive).toBe(true); @@ -80,6 +86,7 @@ describe('PgpmPackage Target API', () => { test('deploys with project:change target', async () => { const mockDeploy = jest.spyOn(project as any, 'deploy'); + mockDeploy.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:procedures/secretfunction'); expect(recursive).toBe(true); @@ -92,6 +99,7 @@ describe('PgpmPackage Target API', () => { test('deploys with project:@tag target', async () => { const mockDeploy = jest.spyOn(project as any, 'deploy'); + mockDeploy.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:@v1.0.0'); expect(recursive).toBe(true); @@ -108,11 +116,13 @@ describe('PgpmPackage Target API', () => { beforeEach(() => { const fixturePath = fixture.getFixturePath('constructive'); + project = new PgpmPackage(fixturePath); }); test('reverts with project-only target', async () => { const mockRevert = jest.spyOn(project as any, 'revert'); + mockRevert.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets'); expect(recursive).toBe(true); @@ -125,6 +135,7 @@ describe('PgpmPackage Target API', () => { test('reverts with project:change target', async () => { const mockRevert = jest.spyOn(project as any, 'revert'); + mockRevert.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:procedures/secretfunction'); expect(recursive).toBe(true); @@ -137,6 +148,7 @@ describe('PgpmPackage Target API', () => { test('reverts with project:@tag target', async () => { const mockRevert = jest.spyOn(project as any, 'revert'); + mockRevert.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:@v1.0.0'); expect(recursive).toBe(true); @@ -153,11 +165,13 @@ describe('PgpmPackage Target API', () => { beforeEach(() => { const fixturePath = fixture.getFixturePath('constructive'); + project = new PgpmPackage(fixturePath); }); test('verifies with project-only target', async () => { const mockVerify = jest.spyOn(project as any, 'verify'); + mockVerify.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets'); expect(recursive).toBe(true); @@ -170,6 +184,7 @@ describe('PgpmPackage Target API', () => { test('verifies with project:change target', async () => { const mockVerify = jest.spyOn(project as any, 'verify'); + mockVerify.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:procedures/secretfunction'); expect(recursive).toBe(true); @@ -182,6 +197,7 @@ describe('PgpmPackage Target API', () => { test('verifies with project:@tag target', async () => { const mockVerify = jest.spyOn(project as any, 'verify'); + mockVerify.mockImplementation(async (opts, target, recursive) => { expect(target).toBe('secrets:@v1.0.0'); expect(recursive).toBe(true); @@ -198,11 +214,13 @@ describe('PgpmPackage Target API', () => { beforeEach(() => { const fixturePath = fixture.getFixturePath('constructive'); + project = new PgpmPackage(fixturePath); }); test('deploy works without target parameter (uses context)', async () => { const mockDeploy = jest.spyOn(project as any, 'deploy'); + mockDeploy.mockImplementation(async (opts, target, recursive) => { expect(target).toBeUndefined(); expect(recursive).toBe(true); @@ -215,6 +233,7 @@ describe('PgpmPackage Target API', () => { test('revert works without target parameter (uses context)', async () => { const mockRevert = jest.spyOn(project as any, 'revert'); + mockRevert.mockImplementation(async (opts, target, recursive) => { expect(target).toBeUndefined(); expect(recursive).toBe(true); @@ -227,6 +246,7 @@ describe('PgpmPackage Target API', () => { test('verify works without target parameter (uses context)', async () => { const mockVerify = jest.spyOn(project as any, 'verify'); + mockVerify.mockImplementation(async (opts, target, recursive) => { expect(target).toBeUndefined(); expect(recursive).toBe(true); diff --git a/pgpm/core/__tests__/core/workspace-extensions-dependency-order.test.ts b/pgpm/core/__tests__/core/workspace-extensions-dependency-order.test.ts index 9d680cd0e..8dd3266e8 100644 --- a/pgpm/core/__tests__/core/workspace-extensions-dependency-order.test.ts +++ b/pgpm/core/__tests__/core/workspace-extensions-dependency-order.test.ts @@ -20,6 +20,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); }); @@ -28,6 +29,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); }); @@ -36,6 +38,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); expect(result).toHaveProperty('resolved'); @@ -49,6 +52,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); expect(Array.isArray(result.resolved)).toBe(true); @@ -60,6 +64,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); expect(result).toHaveProperty('resolved'); @@ -76,6 +81,7 @@ describe('getWorkspaceExtensionsInDependencyOrder', () => { const project = new PgpmPackage(workspacePath); const result = await project.resolveWorkspaceExtensionDependencies(); + expect(result).toMatchSnapshot(); expect(result.resolved.length).toBeGreaterThan(0); diff --git a/pgpm/core/__tests__/files/extension/reader.test.ts b/pgpm/core/__tests__/files/extension/reader.test.ts index ee9c6af9a..d002192df 100644 --- a/pgpm/core/__tests__/files/extension/reader.test.ts +++ b/pgpm/core/__tests__/files/extension/reader.test.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; + import { getInstalledExtensions } from '../../../src/files/extension/reader'; describe('getInstalledExtensions', () => { @@ -24,9 +25,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual([]); }); @@ -34,6 +37,7 @@ superuser = false const nonExistentPath = path.join(tempDir, 'nonexistent.control'); const result = getInstalledExtensions(nonExistentPath); + expect(result).toEqual([]); }); @@ -46,9 +50,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual(['pgcrypto']); }); @@ -61,9 +67,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual(['pgcrypto', 'postgis', 'plpgsql']); }); @@ -76,9 +84,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual(['pgcrypto', 'postgis', 'plpgsql']); }); @@ -91,9 +101,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual(['pgcrypto', 'postgis']); }); @@ -106,9 +118,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual([]); }); @@ -121,9 +135,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual(['pgcrypto', 'postgis']); }); @@ -141,9 +157,11 @@ module_pathname = '$libdir/test' relocatable = false superuser = false `; + fs.writeFileSync(controlFilePath, content); const result = getInstalledExtensions(controlFilePath); + expect(result).toEqual([]); }); }); diff --git a/pgpm/core/__tests__/files/extension/writer.test.ts b/pgpm/core/__tests__/files/extension/writer.test.ts index 960bf6972..1c9e21541 100644 --- a/pgpm/core/__tests__/files/extension/writer.test.ts +++ b/pgpm/core/__tests__/files/extension/writer.test.ts @@ -1,8 +1,9 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { writeExtensions, generateControlFileContent } from '../../../src/files/extension/writer'; + import { getInstalledExtensions } from '../../../src/files/extension/reader'; +import { generateControlFileContent,writeExtensions } from '../../../src/files/extension/writer'; describe('extension writer', () => { let tempDir: string; @@ -111,10 +112,12 @@ describe('extension writer', () => { expect(fs.existsSync(makefile)).toBe(true); const controlContent = fs.readFileSync(controlFile, 'utf-8'); + expect(controlContent).toContain("requires = 'pgcrypto'"); expect(controlContent).toContain("default_version = '1.0.0'"); const makefileContent = fs.readFileSync(makefile, 'utf-8'); + expect(makefileContent).toContain('EXTENSION = test-module'); expect(makefileContent).toContain('DATA = sql/test-module--1.0.0.sql'); }); @@ -128,6 +131,7 @@ describe('extension writer', () => { expect(controlContent).toContain("requires = 'pgcrypto,postgis,plpgsql'"); const extensions = getInstalledExtensions(controlFile); + expect(extensions).toEqual(['pgcrypto', 'postgis', 'plpgsql']); }); @@ -140,14 +144,16 @@ describe('extension writer', () => { expect(controlContent).not.toContain('requires ='); const extensions = getInstalledExtensions(controlFile); + expect(extensions).toEqual([]); }); it('updates existing control file with new extensions', () => { writeExtensions(packageDir, ['pgcrypto']); - let controlFile = path.join(packageDir, 'test-module.control'); + const controlFile = path.join(packageDir, 'test-module.control'); let extensions = getInstalledExtensions(controlFile); + expect(extensions).toEqual(['pgcrypto']); writeExtensions(packageDir, ['pgcrypto', 'postgis']); @@ -165,6 +171,7 @@ describe('extension writer', () => { writeExtensions(packageDir, ['pgcrypto', 'postgis']); const secondContent = fs.readFileSync(controlFile, 'utf-8'); + expect(secondContent).toBe(firstContent); }); @@ -173,6 +180,7 @@ describe('extension writer', () => { const controlFile = path.join(packageDir, 'test-module.control'); let extensions = getInstalledExtensions(controlFile); + expect(extensions).toEqual(['pgcrypto']); writeExtensions(packageDir, ['postgis', 'plpgsql']); @@ -190,6 +198,7 @@ describe('extension writer', () => { expect(controlContent).toContain("requires = 'plpgsql,pgcrypto,postgis'"); const extensions = getInstalledExtensions(controlFile); + expect(extensions).toEqual(['plpgsql', 'pgcrypto', 'postgis']); }); }); diff --git a/pgpm/core/__tests__/files/plan/plan-file-parsing.test.ts b/pgpm/core/__tests__/files/plan/plan-file-parsing.test.ts index c6c41497a..b3c2c76aa 100644 --- a/pgpm/core/__tests__/files/plan/plan-file-parsing.test.ts +++ b/pgpm/core/__tests__/files/plan/plan-file-parsing.test.ts @@ -31,6 +31,7 @@ users_table 2024-01-01T00:00:00Z Developer # Create users tabl posts_table [users_table] 2024-01-02T00:00:00Z Developer # Create posts table `; const planPath = join(testDir, 'simple.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -54,6 +55,7 @@ users_table 2024-01-01T00:00:00Z Developer # Create users tabl posts_table [users_table] 2024-01-03T00:00:00Z Developer # Create posts table `; const planPath = join(testDir, 'with-tags.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -73,6 +75,7 @@ users_table 2024-01-01T00:00:00Z Developer # Create users tabl posts_table [users_table !old_posts] 2024-01-02T00:00:00Z Developer # Create posts table `; const planPath = join(testDir, 'with-conflicts.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -90,6 +93,7 @@ invalid line without timestamp users_table 2024-01-01T00:00:00Z Developer `; const planPath = join(testDir, 'invalid.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -106,6 +110,7 @@ users-table 2024-01-01T00:00:00Z Developer # Valid name users@table 2024-01-02T00:00:00Z Developer # Invalid name `; const planPath = join(testDir, 'invalid-names.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -122,6 +127,7 @@ users_table 2024-01-01T00:00:00Z Developer posts_table [@invalid@tag users_table] 2024-01-02T00:00:00Z Developer `; const planPath = join(testDir, 'invalid-deps.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFile(planPath); @@ -141,6 +147,7 @@ users_table 2024-01-01T00:00:00Z Developer posts_table 2024-01-03T00:00:00Z Developer `; const planPath = join(testDir, 'simple-parse.plan'); + writeFileSync(planPath, planContent); const result = parsePlanFileSimple(planPath); @@ -165,6 +172,7 @@ posts_table 2024-01-03T00:00:00Z Developer comments_table 2024-01-04T00:00:00Z Developer `; const planPath = join(testDir, 'get-changes.plan'); + writeFileSync(planPath, planContent); const changes = getChanges(planPath); @@ -183,6 +191,7 @@ posts_table 2024-01-02T00:00:00Z Developer comments_table 2024-01-03T00:00:00Z Developer `; const planPath = join(testDir, 'latest-change.plan'); + writeFileSync(planPath, planContent); const latest = getLatestChange(planPath); @@ -195,6 +204,7 @@ comments_table 2024-01-03T00:00:00Z Developer %project=test-project `; const planPath = join(testDir, 'no-changes.plan'); + writeFileSync(planPath, planContent); expect(getLatestChange(planPath)).toBe(''); @@ -218,6 +228,7 @@ comments_table 2024-01-03T00:00:00Z Developer it('should resolve change references', () => { const result = resolveReference('posts_table', plan); + expect(result.change).toBeDefined(); expect(result.change).toBe('posts_table'); expect(result.tag).toBeUndefined(); @@ -225,6 +236,7 @@ comments_table 2024-01-03T00:00:00Z Developer it('should resolve tag references', () => { const result = resolveReference('@v1.0.0', plan); + expect(result.tag).toBeDefined(); expect(result.tag).toBe('v1.0.0'); expect(result.change).toBe('users_table'); @@ -232,24 +244,28 @@ comments_table 2024-01-03T00:00:00Z Developer it('should resolve HEAD reference', () => { const result = resolveReference('HEAD', plan); + expect(result.change).toBeDefined(); expect(result.change).toBe('comments_table'); }); it('should resolve ROOT reference', () => { const result = resolveReference('ROOT', plan); + expect(result.change).toBeDefined(); expect(result.change).toBe('users_table'); }); it('should resolve relative references', () => { const result = resolveReference('HEAD^', plan); + expect(result.change).toBeDefined(); expect(result.change).toBe('posts_table'); }); it('should resolve tag-relative references', () => { const result = resolveReference('@v2.0.0~', plan); + expect(result.change).toBeDefined(); expect(result.change).toBe('comments_table'); }); diff --git a/pgpm/core/__tests__/files/plan/plan-parsing-invalid-fixtures.test.ts b/pgpm/core/__tests__/files/plan/plan-parsing-invalid-fixtures.test.ts index cdcf1fdd2..c9b7a83e4 100644 --- a/pgpm/core/__tests__/files/plan/plan-parsing-invalid-fixtures.test.ts +++ b/pgpm/core/__tests__/files/plan/plan-parsing-invalid-fixtures.test.ts @@ -2,15 +2,18 @@ import { TestPlan } from '../../../test-utils'; it('plan-invalid/bad-symbolic-refs.plan', () => { const testPlan = new TestPlan('plan-invalid/bad-symbolic-refs.plan'); + expect(testPlan.result.errors).toMatchSnapshot(); }); it('plan-invalid/invalid-change-names.plan', () => { const testPlan = new TestPlan('plan-invalid/invalid-change-names.plan'); + expect(testPlan.result.errors).toMatchSnapshot(); }); it('plan-invalid/invalid-tag-names.plan', () => { const testPlan = new TestPlan('plan-invalid/invalid-tag-names.plan'); + expect(testPlan.result.errors).toMatchSnapshot(); }); \ No newline at end of file diff --git a/pgpm/core/__tests__/files/plan/plan-parsing-valid-fixtures.test.ts b/pgpm/core/__tests__/files/plan/plan-parsing-valid-fixtures.test.ts index ce45e3bc7..1ebfab231 100644 --- a/pgpm/core/__tests__/files/plan/plan-parsing-valid-fixtures.test.ts +++ b/pgpm/core/__tests__/files/plan/plan-parsing-valid-fixtures.test.ts @@ -2,30 +2,36 @@ import { TestPlan } from '../../../test-utils'; it('plan-valid/project-qualified.plan', () => { const testPlan = new TestPlan('plan-valid/project-qualified.plan'); + expect(testPlan.result.errors.length).toBe(0); }); it('plan-valid/relative-head-root.plan', () => { const testPlan = new TestPlan('plan-valid/relative-head-root.plan'); + expect(testPlan.result.errors.length).toBe(0); }); it('plan-valid/sha1-refs.plan', () => { const testPlan = new TestPlan('plan-valid/sha1-refs.plan'); + expect(testPlan.result.errors.length).toBe(0); }); it('plan-valid/symbolic-head-root.plan', () => { const testPlan = new TestPlan('plan-valid/symbolic-head-root.plan'); + expect(testPlan.result.errors.length).toBe(0); }); it('plan-valid/valid-change-names.plan', () => { const testPlan = new TestPlan('plan-valid/valid-change-names.plan'); + expect(testPlan.result.errors.length).toBe(0); }); it('plan-valid/valid-tag-names.plan', () => { const testPlan = new TestPlan('plan-valid/valid-tag-names.plan'); + expect(testPlan.result.errors.length).toBe(0); }); \ No newline at end of file diff --git a/pgpm/core/__tests__/files/plan/plan-validation-utilities.test.ts b/pgpm/core/__tests__/files/plan/plan-validation-utilities.test.ts index 2799c2118..fab032732 100644 --- a/pgpm/core/__tests__/files/plan/plan-validation-utilities.test.ts +++ b/pgpm/core/__tests__/files/plan/plan-validation-utilities.test.ts @@ -88,6 +88,7 @@ describe('Validators', () => { describe('parseReference', () => { it('should parse plain change names', () => { const ref = parseReference('users_table'); + expect(ref).toMatchObject({ change: 'users_table' }); @@ -95,6 +96,7 @@ describe('Validators', () => { it('should parse tag references', () => { const ref = parseReference('@v1.0.0'); + expect(ref).toMatchObject({ tag: 'v1.0.0' }); @@ -102,6 +104,7 @@ describe('Validators', () => { it('should parse change at tag', () => { const ref = parseReference('users_table@v1.0.0'); + expect(ref).toMatchObject({ change: 'users_table', tag: 'v1.0.0' @@ -111,13 +114,15 @@ describe('Validators', () => { it('should parse SHA1 references', () => { const sha1 = 'abc1234567890123456789012345678901234567'; const ref = parseReference(sha1); + expect(ref).toMatchObject({ - sha1: sha1 + sha1 }); }); it('should parse project-qualified references', () => { const ref = parseReference('other_project:users_table'); + expect(ref).toMatchObject({ package: 'other_project', change: 'users_table' @@ -171,6 +176,7 @@ describe('Validators', () => { it('should parse conflict references', () => { const ref = parseReference('!users_table'); + expect(ref).toBeNull(); // Conflicts are not parsed by parseReference }); diff --git a/pgpm/core/__tests__/files/plan/stage-unique-names-plan.test.ts b/pgpm/core/__tests__/files/plan/stage-unique-names-plan.test.ts index 77be03763..7f3327da3 100644 --- a/pgpm/core/__tests__/files/plan/stage-unique-names-plan.test.ts +++ b/pgpm/core/__tests__/files/plan/stage-unique-names-plan.test.ts @@ -14,17 +14,20 @@ describe('stage fixture plan generation (unique-names)', () => { it('generates a plan for unique-names (no packages)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); const plan = mod.generateModulePlan({ includePackages: false }); + expect(cleanText(plan)).toMatchSnapshot(); }); it('generates a plan for unique-names (with packages)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); const plan = mod.generateModulePlan({ includePackages: true }); + expect(cleanText(plan)).toMatchSnapshot(); }); it('generates a plan for unique-names (with packages and includeTags)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); const plan = mod.generateModulePlan({ includePackages: true, includeTags: true }); + expect(cleanText(plan)).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/files/plan/writer.test.ts b/pgpm/core/__tests__/files/plan/writer.test.ts index 41b4ade9c..e30d9a1a8 100644 --- a/pgpm/core/__tests__/files/plan/writer.test.ts +++ b/pgpm/core/__tests__/files/plan/writer.test.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; + import { writeSqitchPlan } from '../../../src/files/plan/writer'; import { SqitchRow } from '../../../src/files/types'; @@ -49,9 +50,11 @@ describe('writeSqitchPlan', () => { writeSqitchPlan(rows, opts); const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + expect(fs.existsSync(planPath)).toBe(true); const content = fs.readFileSync(planPath, 'utf-8'); + expect(content).toContain('%project=test-module'); expect(content).toContain('John Doe '); expect(content).toContain('schemas/test/schema 2017-08-11T08:11:51Z John Doe '); @@ -183,6 +186,7 @@ describe('writeSqitchPlan', () => { // Should only appear once const matches = content.match(/schemas\/test\/schema/g); + expect(matches?.length).toBe(1); // Only in the header line, not duplicated expect(consoleSpy).toHaveBeenCalledWith('DUPLICATE schemas/test/schema'); diff --git a/pgpm/core/__tests__/migrate/tag-fallback.test.ts b/pgpm/core/__tests__/migrate/tag-fallback.test.ts index cea5c587d..dea7acffe 100644 --- a/pgpm/core/__tests__/migrate/tag-fallback.test.ts +++ b/pgpm/core/__tests__/migrate/tag-fallback.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, writeFileSync, mkdirSync } from 'fs'; +import { mkdirSync,mkdtempSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -26,6 +26,7 @@ jest.mock('pg-cache', () => ({ if (typeof sql === 'string' && sql.includes('SELECT pgpm_migrate.is_deployed')) { return Promise.resolve({ rows: [{ is_deployed: false }] }); } + return Promise.resolve({ rows: [] }); }), }), @@ -61,6 +62,7 @@ describe('PgpmMigrate.deploy tag fallback bug reproduction', () => { } as any); const calls = (executeQuery as jest.Mock).mock.calls; + expect(calls.length).toBeGreaterThan(0); const params = calls[0][2]; const deps = params[3]; diff --git a/pgpm/core/__tests__/migration/basic-deployment.test.ts b/pgpm/core/__tests__/migration/basic-deployment.test.ts index c84c4926d..ae81eb64c 100644 --- a/pgpm/core/__tests__/migration/basic-deployment.test.ts +++ b/pgpm/core/__tests__/migration/basic-deployment.test.ts @@ -35,6 +35,7 @@ describe('Deploy Command', () => { // Verify it was recorded const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(1); expect(deployed[0]).toMatchObject({ package: 'test-simple', @@ -59,9 +60,11 @@ describe('Deploy Command', () => { // Verify dependencies were recorded const tableDeps = await db.getDependencies('test-simple', 'table'); + expect(tableDeps).toContain('test-simple:schema'); const indexDeps = await db.getDependencies('test-simple', 'index'); + expect(indexDeps).toContain('test-simple:table'); }); @@ -111,6 +114,7 @@ describe('Deploy Command', () => { // Verify nothing was deployed due to rollback const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(0); // Verify table doesn't exist @@ -144,6 +148,7 @@ describe('Deploy Command', () => { // Verify first change was deployed const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(1); expect(deployed[0].change_name).toBe('good_change'); diff --git a/pgpm/core/__tests__/migration/cross-project-dependencies.test.ts b/pgpm/core/__tests__/migration/cross-project-dependencies.test.ts index a4686e1fe..4cde19e75 100644 --- a/pgpm/core/__tests__/migration/cross-project-dependencies.test.ts +++ b/pgpm/core/__tests__/migration/cross-project-dependencies.test.ts @@ -44,9 +44,11 @@ describe('Cross-Project Dependencies', () => { // Verify cross-project dependencies were recorded const appSchemaDeps = await db.getDependencies('project-b', 'app_schema'); + expect(appSchemaDeps).toContain('project-a:base_schema'); const appTablesDeps = await db.getDependencies('project-b', 'app_tables'); + expect(appTablesDeps).toContain('project-a:base_types'); }); @@ -60,6 +62,7 @@ describe('Cross-Project Dependencies', () => { // Verify nothing was deployed const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(0); }); @@ -85,6 +88,7 @@ describe('Cross-Project Dependencies', () => { // Verify nothing was reverted expect(await db.exists('schema', 'base')).toBe(true); const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(4); // All 4 changes still deployed }); @@ -159,6 +163,7 @@ describe('Cross-Project Dependencies', () => { // Verify all deployed const deployed = await db.getDeployedChanges(); + expect(deployed).toHaveLength(6); // Try to revert a change that many depend on diff --git a/pgpm/core/__tests__/migration/stage-deployment-with-plan.test.ts b/pgpm/core/__tests__/migration/stage-deployment-with-plan.test.ts index 5b2ff7519..4b8ad4c43 100644 --- a/pgpm/core/__tests__/migration/stage-deployment-with-plan.test.ts +++ b/pgpm/core/__tests__/migration/stage-deployment-with-plan.test.ts @@ -22,6 +22,7 @@ describe('Staging Fixture Deployment With Pre-Generated Plan', () => { test('deploys unique-names after generating plan (includePackages=true, includeTags=true)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); + mod.writeModulePlan({ includePackages: true, includeTags: true }); await fixture.deployModule('unique-names', db.name, ['stage']); @@ -30,11 +31,13 @@ describe('Staging Fixture Deployment With Pre-Generated Plan', () => { expect(await db.exists('table', 'unique_names.words')).toBe(true); const deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges.some(change => change.package === 'unique-names')).toBe(true); }); test('deploys unique-names after generating plan (includePackages=true, includeTags=false)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); + mod.writeModulePlan({ includePackages: true, includeTags: false }); await fixture.deployModule('unique-names', db.name, ['stage']); @@ -43,15 +46,18 @@ describe('Staging Fixture Deployment With Pre-Generated Plan', () => { expect(await db.exists('table', 'unique_names.words')).toBe(true); const deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges.some(change => change.package === 'unique-names')).toBe(true); }); test('deploys unique-names after generating plan (includePackages=false, includeTags=false)', async () => { const mod = fixture.getModuleProject([], 'unique-names'); + mod.writeModulePlan({ includePackages: false, includeTags: false }); await fixture.deployModule('unique-names', db.name, ['stage']); expect(await db.exists('schema', 'unique_names')).toBe(true); expect(await db.exists('table', 'unique_names.words')).toBe(true); const deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges.some(change => change.package === 'unique-names')).toBe(true); }); diff --git a/pgpm/core/__tests__/migration/stage-deployment.test.ts b/pgpm/core/__tests__/migration/stage-deployment.test.ts index b746bc710..8cae68c8b 100644 --- a/pgpm/core/__tests__/migration/stage-deployment.test.ts +++ b/pgpm/core/__tests__/migration/stage-deployment.test.ts @@ -27,6 +27,7 @@ describe('Staging Fixture Deployment Tests', () => { expect(await db.exists('table', 'unique_names.words')).toBe(true); const deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges.some(change => change.package === 'unique-names')).toBe(true); }); }); diff --git a/pgpm/core/__tests__/migration/tag-based-migration.test.ts b/pgpm/core/__tests__/migration/tag-based-migration.test.ts index bffd3b68a..1195871b2 100644 --- a/pgpm/core/__tests__/migration/tag-based-migration.test.ts +++ b/pgpm/core/__tests__/migration/tag-based-migration.test.ts @@ -54,13 +54,16 @@ describe('Simple with Tags Migration', () => { expect(await db.exists('table', 'mythirdapp.customers')).toBe(true); const createSchemaDeps = await db.getDependencies('my-third', 'create_schema'); + expect(createSchemaDeps).toContain('my-first:table_products'); // resolved from my-first:@v1.1.0 expect(createSchemaDeps).toContain('my-second:create_table'); // resolved from my-second:@v2.0.0 const createTableDeps = await db.getDependencies('my-third', 'create_table'); + expect(createTableDeps).toContain('my-third:create_schema'); const deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges).toContainEqual(expect.objectContaining({ package: 'my-first', change_name: 'schema_myfirstapp' @@ -122,6 +125,7 @@ describe('Simple with Tags Migration', () => { expect(await db.exists('table', 'mythirdapp.customers')).toBe(true); const createSchemaDeps = await db.getDependencies('my-third', 'create_schema'); + expect(createSchemaDeps).toContain('my-first:table_products'); // resolved from my-first:@v1.1.0 expect(createSchemaDeps).toContain('my-second:create_table'); // resolved from my-second:@v2.0.0 }); @@ -229,6 +233,7 @@ describe('Simple with Tags Migration', () => { // Verify final state: my-third dependencies should still be resolved correctly after the complex sequence const createSchemaDeps = await db.getDependencies('my-third', 'create_schema'); + expect(createSchemaDeps).toContain('my-first:table_products'); // resolved from my-first:@v1.1.0 expect(createSchemaDeps).toContain('my-second:create_table'); // resolved from my-second:@v2.0.0 @@ -265,6 +270,7 @@ describe('Simple with Tags Migration', () => { // Verify initial state: all projects fully deployed let deployedChanges = await db.getDeployedChanges(); + expect(deployedChanges.filter(c => c.package === 'my-first')).toHaveLength(3); expect(deployedChanges.filter(c => c.package === 'my-second')).toHaveLength(3); expect(deployedChanges.filter(c => c.package === 'my-third')).toHaveLength(2); @@ -286,6 +292,7 @@ describe('Simple with Tags Migration', () => { // Verify state after revert to v1.0.0 deployedChanges = await db.getDeployedChanges(); const myFirstChanges = deployedChanges.filter(c => c.package === 'my-first'); + expect(myFirstChanges).toHaveLength(2); // schema_myfirstapp, table_users expect(myFirstChanges.map(c => c.change_name)).toEqual(['schema_myfirstapp', 'table_users']); @@ -301,6 +308,7 @@ describe('Simple with Tags Migration', () => { // Verify state remains at my-second v2.0.0 level (all changes deployed) deployedChanges = await db.getDeployedChanges(); const mySecondChanges = deployedChanges.filter(c => c.package === 'my-second'); + expect(mySecondChanges).toHaveLength(3); // create_schema, create_table, create_another_table expect(mySecondChanges.map(c => c.change_name)).toEqual(['create_schema', 'create_table', 'create_another_table']); @@ -316,6 +324,7 @@ describe('Simple with Tags Migration', () => { // Verify state - my-second remains fully deployed due to fixture limitation deployedChanges = await db.getDeployedChanges(); const mySecondChangesAfterRevert = deployedChanges.filter(c => c.package === 'my-second'); + expect(mySecondChangesAfterRevert).toHaveLength(3); // all changes remain due to fixture limitation expect(mySecondChangesAfterRevert.map(c => c.change_name)).toEqual(['create_schema', 'create_table', 'create_another_table']); @@ -346,6 +355,7 @@ describe('Simple with Tags Migration', () => { // Verify final state: all dependencies correctly resolved const createSchemaDeps = await db.getDependencies('my-third', 'create_schema'); + expect(createSchemaDeps).toContain('my-first:table_products'); // resolved from my-first:@v1.1.0 expect(createSchemaDeps).toContain('my-second:create_table'); // resolved from my-second:@v2.0.0 @@ -392,6 +402,7 @@ describe('Simple with Tags Migration', () => { // Verify both tag formats resolve to the same changes when appropriate const deployedChanges = await db.getDeployedChanges(); const mySecondChanges = deployedChanges.filter(c => c.package === 'my-second'); + expect(mySecondChanges).toHaveLength(3); // create_schema, create_table, create_another_table expect(mySecondChanges.map(c => c.change_name)).toEqual(['create_schema', 'create_table', 'create_another_table']); }); diff --git a/pgpm/core/__tests__/modules/module-installation.test.ts b/pgpm/core/__tests__/modules/module-installation.test.ts index df67e8d54..e836d4a4f 100644 --- a/pgpm/core/__tests__/modules/module-installation.test.ts +++ b/pgpm/core/__tests__/modules/module-installation.test.ts @@ -37,10 +37,12 @@ describe('installModule()', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(mod.getModulePath()!, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies).toBeDefined(); expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.2.0'); const controlFileContent = mod.getModuleControlFile(); + expect(controlFileContent).toMatchSnapshot(); }); @@ -56,6 +58,7 @@ describe('installModule()', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(mod.getModulePath()!, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/totp']).toBe('1.2.0'); }); @@ -147,6 +150,7 @@ describe('upgradeModules()', () => { const pkgJson = JSON.parse( fs.readFileSync(path.join(mod.getModulePath()!, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.1.0'); }); @@ -156,6 +160,7 @@ describe('upgradeModules()', () => { let pkgJson = JSON.parse( fs.readFileSync(path.join(mod.getModulePath()!, 'package.json'), 'utf-8') ); + expect(pkgJson.dependencies['@pgpm-testing/base32']).toBe('1.1.0'); const result = await mod.upgradeModules(); diff --git a/pgpm/core/__tests__/modules/module-packaging-tags.test.ts b/pgpm/core/__tests__/modules/module-packaging-tags.test.ts index f4f3ed2d1..20bcfa841 100644 --- a/pgpm/core/__tests__/modules/module-packaging-tags.test.ts +++ b/pgpm/core/__tests__/modules/module-packaging-tags.test.ts @@ -17,30 +17,36 @@ afterAll(() => { it('my-first extension', async () => { const { sql } = await packageModule(fixtures[0].tempFixtureDir); + expect(sql).toMatchSnapshot(); }); it('my-first extension via plan', async () => { const { sql } = await packageModule(fixtures[0].tempFixtureDir, { usePlan: true }); + expect(sql).toMatchSnapshot(); }); it('my-second extension', async () => { const { sql } = await packageModule(fixtures[1].tempFixtureDir); + expect(sql).toMatchSnapshot(); }); it('my-second extension via plan', async () => { const { sql } = await packageModule(fixtures[1].tempFixtureDir, { usePlan: true }); + expect(sql).toMatchSnapshot(); }); it('my-third extension', async () => { const { sql } = await packageModule(fixtures[2].tempFixtureDir); + expect(sql).toMatchSnapshot(); }); it('my-third extension via plan', async () => { const { sql } = await packageModule(fixtures[2].tempFixtureDir, { usePlan: true }); + expect(sql).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/modules/module-packaging.test.ts b/pgpm/core/__tests__/modules/module-packaging.test.ts index f2d1a420d..0dd6afb37 100644 --- a/pgpm/core/__tests__/modules/module-packaging.test.ts +++ b/pgpm/core/__tests__/modules/module-packaging.test.ts @@ -14,11 +14,13 @@ afterAll(() => { describe('packageModule tests', () => { it('creates an extension', async () => { const { sql } = await packageModule(fixture.tempFixtureDir); + expect(sql).toMatchSnapshot(); }); it('creates an extension via plan', async () => { const { sql } = await packageModule(fixture.tempFixtureDir, { usePlan: true }); + expect(sql).toMatchSnapshot(); }); }); diff --git a/pgpm/core/__tests__/modules/module-publishing.test.ts b/pgpm/core/__tests__/modules/module-publishing.test.ts index 31897bf16..c1b284a67 100644 --- a/pgpm/core/__tests__/modules/module-publishing.test.ts +++ b/pgpm/core/__tests__/modules/module-publishing.test.ts @@ -62,6 +62,7 @@ describe('publishToDist()', () => { it('skips extraneous files and folders', () => { const junkDir = path.join(fixture.tempFixtureDir, 'junk'); + fs.mkdirSync(junkDir, { recursive: true }); fs.writeFileSync(path.join(junkDir, 'ignore.txt'), 'junk'); diff --git a/pgpm/core/__tests__/projects/clear-functionality.test.ts b/pgpm/core/__tests__/projects/clear-functionality.test.ts index 2814b36ed..49f36299a 100644 --- a/pgpm/core/__tests__/projects/clear-functionality.test.ts +++ b/pgpm/core/__tests__/projects/clear-functionality.test.ts @@ -1,8 +1,8 @@ import fs from 'fs'; import path from 'path'; -import { PgpmPackage } from '../../src/core/class/pgpm'; -import { TestFixture } from '../../test-utils/TestFixture'; + import { parsePlanFile } from '../../src/files/plan/parser'; +import { TestFixture } from '../../test-utils/TestFixture'; describe('Clear Functionality', () => { let fixture: TestFixture; @@ -34,16 +34,20 @@ describe('Clear Functionality', () => { expect(fs.existsSync(path.join(deployDir, 'table_products.sql'))).toBe(true); const result = parsePlanFile(planPath); + expect(result.errors.length).toBe(0); const plan = result.data!; + expect(plan.changes.length).toBe(3); const firstChange = plan.changes[0].name; + expect(firstChange).toBe('schema_myfirstapp'); await pkg.removeFromPlan(firstChange); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); @@ -67,11 +71,14 @@ describe('Clear Functionality', () => { await pkg.removeFromPlan('schema_myfirstapp'); const result = parsePlanFile(planPath); + expect(result.errors.length).toBe(0); const plan = result.data!; + expect(plan.changes.length).toBe(0); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); @@ -85,15 +92,19 @@ describe('Clear Functionality', () => { expect(result.errors.length).toBe(0); const plan = result.data!; + expect(plan.changes.length).toBeGreaterThan(0); const firstChange = plan.changes[0].name; + expect(firstChange).toBe('schema_myfirstapp'); const secondChange = plan.changes[1].name; + expect(secondChange).toBe('table_users'); const thirdChange = plan.changes[2].name; + expect(thirdChange).toBe('table_products'); }); @@ -113,6 +124,7 @@ describe('Clear Functionality', () => { await pkg.removeFromPlan(firstChange); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('@v1.0.0'); expect(updatedPlan).not.toContain('@v1.1.0'); }); @@ -136,6 +148,7 @@ describe('Clear Functionality', () => { await expect(pkg.removeFromPlan(firstChange)).resolves.not.toThrow(); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); diff --git a/pgpm/core/__tests__/projects/deploy-failure-scenarios.test.ts b/pgpm/core/__tests__/projects/deploy-failure-scenarios.test.ts index 7081bca22..f585cb748 100644 --- a/pgpm/core/__tests__/projects/deploy-failure-scenarios.test.ts +++ b/pgpm/core/__tests__/projects/deploy-failure-scenarios.test.ts @@ -53,6 +53,7 @@ describe('Deploy Failure Scenarios', () => { const client = new PgpmMigrate(db.config); const initialState = await db.getMigrationState(); + expect(initialState.changeCount).toBe(0); expect(initialState.eventCount).toBe(0); @@ -115,6 +116,7 @@ describe('Deploy Failure Scenarios', () => { const client = new PgpmMigrate(db.config); const initialState = await db.getMigrationState(); + expect(initialState.changeCount).toBe(0); await expect(client.deploy({ @@ -131,15 +133,19 @@ describe('Deploy Failure Scenarios', () => { expect(await db.exists('table', 'test_products')).toBe(true); const records = await db.query('SELECT * FROM test_products'); + expect(records.rows).toHaveLength(1); expect(records.rows[0].sku).toBe('PROD-001'); const finalRecord = await db.query("SELECT * FROM test_products WHERE sku = 'PROD-002'"); + expect(finalRecord.rows).toHaveLength(0); const successEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && !e.error_message); + expect(successEvents.length).toBe(2); // create_table, add_record const failEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && e.error_message); + expect(failEvents.length).toBe(1); // violate_constraint failure logged expect(finalState.eventCount).toBe(3); // 2 successful deployments + 1 failure }); @@ -252,14 +258,17 @@ describe('Deploy Failure Scenarios', () => { expect(await db.exists('table', 'test_schema.orders')).toBe(true); const records = await db.query('SELECT * FROM test_schema.orders'); + expect(records.rows).toHaveLength(0); expect(finalState.changeCount).toBe(2); expect(finalState.changes.map((c: any) => c.change_name)).toEqual(['setup_schema', 'create_constraint_table']); const successEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && !e.error_message); + expect(successEvents.length).toBe(2); // setup_schema, create_constraint_table const failEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && e.error_message); + expect(failEvents.length).toBe(1); // fail_on_constraint failure logged expect(finalState.eventCount).toBe(3); // 2 successful deployments + 1 failure @@ -310,6 +319,7 @@ describe('Deploy Failure Scenarios', () => { }); const deployState = await db.getMigrationState(); + expect(deployState.changeCount).toBe(1); expect(await db.exists('table', 'users')).toBe(true); @@ -323,6 +333,7 @@ describe('Deploy Failure Scenarios', () => { // Should have deploy success event + verify failure event const verifyEvents = finalState.events.filter((e: any) => e.event_type === 'verify'); + expect(verifyEvents.length).toBe(1); expect(verifyEvents[0].error_message).toBe('Verification failed for create_simple_table'); expect(verifyEvents[0].error_code).toBe('VERIFICATION_FAILED'); diff --git a/pgpm/core/__tests__/projects/deploy-modules.test.ts b/pgpm/core/__tests__/projects/deploy-modules.test.ts index 465b942ae..898373be4 100644 --- a/pgpm/core/__tests__/projects/deploy-modules.test.ts +++ b/pgpm/core/__tests__/projects/deploy-modules.test.ts @@ -29,19 +29,24 @@ describe('Basic Deployment with deployModules', () => { expect(await db.exists('table', 'mythirdapp.customers')).toBe(true); const schemas = await db.query('SELECT schema_name FROM information_schema.schemata WHERE schema_name IN (\'myfirstapp\', \'mysecondapp\', \'mythirdapp\') ORDER BY schema_name'); + expect(schemas.rows).toHaveLength(3); expect(schemas.rows.map((r: any) => r.schema_name)).toEqual(['myfirstapp', 'mysecondapp', 'mythirdapp']); const tables = await db.query('SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema IN (\'myfirstapp\', \'mysecondapp\', \'mythirdapp\') ORDER BY table_schema, table_name'); + expect(tables.rows).toHaveLength(6); const myfirstappTables = tables.rows.filter((r: any) => r.table_schema === 'myfirstapp').map((r: any) => r.table_name); + expect(myfirstappTables.sort()).toEqual(['products', 'users']); const mysecondappTables = tables.rows.filter((r: any) => r.table_schema === 'mysecondapp').map((r: any) => r.table_name); + expect(mysecondappTables.sort()).toEqual(['consent_agreements', 'user_interactions', 'users']); const mythirdappTables = tables.rows.filter((r: any) => r.table_schema === 'mythirdapp').map((r: any) => r.table_name); + expect(mythirdappTables).toEqual(['customers']); }); }); diff --git a/pgpm/core/__tests__/projects/dynamic-plan-modification.test.ts b/pgpm/core/__tests__/projects/dynamic-plan-modification.test.ts index 438278aaf..445a520eb 100644 --- a/pgpm/core/__tests__/projects/dynamic-plan-modification.test.ts +++ b/pgpm/core/__tests__/projects/dynamic-plan-modification.test.ts @@ -26,7 +26,8 @@ describe('Forked Deployment with deployModules', () => { const verifyDir = join(packagePath, 'verify'); const originalPlan = readFileSync(planPath, 'utf8'); - const modifiedPlan = originalPlan.trimEnd() + '\ntable_orders [table_products] 2017-08-11T08:11:51Z constructive # add table_orders\n'; + const modifiedPlan = `${originalPlan.trimEnd() }\ntable_orders [table_products] 2017-08-11T08:11:51Z constructive # add table_orders\n`; + writeFileSync(planPath, modifiedPlan); writeFileSync(join(deployDir, 'table_orders.sql'), `-- Deploy my-first:table_orders to pg @@ -68,10 +69,12 @@ SELECT 1/count(*) FROM information_schema.tables WHERE table_schema = 'myfirstap expect(await db.exists('table', 'myfirstapp.orders')).toBe(true); const schemas = await db.query('SELECT schema_name FROM information_schema.schemata WHERE schema_name = \'myfirstapp\''); + expect(schemas.rows).toHaveLength(1); expect(schemas.rows[0].schema_name).toBe('myfirstapp'); const tables = await db.query('SELECT table_name FROM information_schema.tables WHERE table_schema = \'myfirstapp\' ORDER BY table_name'); + expect(tables.rows).toHaveLength(3); expect(tables.rows.map((r: any) => r.table_name)).toEqual(['orders', 'products', 'users']); diff --git a/pgpm/core/__tests__/projects/forked-deployment-scenarios.test.ts b/pgpm/core/__tests__/projects/forked-deployment-scenarios.test.ts index 9d0f7b81b..34c89b10b 100644 --- a/pgpm/core/__tests__/projects/forked-deployment-scenarios.test.ts +++ b/pgpm/core/__tests__/projects/forked-deployment-scenarios.test.ts @@ -2,6 +2,7 @@ process.env.CONSTRUCTIVE_DEBUG = 'true'; import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; + import { TestDatabase } from '../../test-utils'; import { CoreDeployTestFixture } from '../../test-utils/CoreDeployTestFixture'; @@ -51,6 +52,7 @@ describe('Forked Deployment with deployModules - my-third', () => { 'updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()', 'updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),\n category TEXT DEFAULT \'general\'' ); + writeFileSync(tableProductsPath, modifiedTableProducts); await fixture.deployModule('my-third', db.name, ['sqitch', 'simple-w-tags']); @@ -60,6 +62,7 @@ describe('Forked Deployment with deployModules - my-third', () => { expect(await db.exists('table', 'myfirstapp.products')).toBe(true); const columns = await db.query('SELECT column_name FROM information_schema.columns WHERE table_schema = \'myfirstapp\' AND table_name = \'products\' AND column_name = \'category\''); + expect(columns.rows).toHaveLength(1); expect(columns.rows[0].column_name).toBe('category'); diff --git a/pgpm/core/__tests__/projects/remove-functionality.test.ts b/pgpm/core/__tests__/projects/remove-functionality.test.ts index fbd26aab8..4c2fab8ec 100644 --- a/pgpm/core/__tests__/projects/remove-functionality.test.ts +++ b/pgpm/core/__tests__/projects/remove-functionality.test.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { PgpmPackage } from '../../src/core/class/pgpm'; + import { TestFixture } from '../../test-utils/TestFixture'; describe('Remove Functionality', () => { @@ -41,6 +41,7 @@ describe('Remove Functionality', () => { await pkg.removeFromPlan('schema_myfirstapp'); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); @@ -69,6 +70,7 @@ describe('Remove Functionality', () => { await pkg.removeFromPlan('table_users'); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); @@ -103,6 +105,7 @@ describe('Remove Functionality', () => { const planPath = path.join(pkg.getModulePath()!, 'pgpm.plan'); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); @@ -124,6 +127,7 @@ describe('Remove Functionality', () => { await pkg.removeFromPlan('table_users'); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('@v1.0.0'); expect(updatedPlan).not.toContain('@v1.1.0'); }); @@ -170,6 +174,7 @@ describe('Remove Functionality', () => { const pkg = fixture.getModuleProject([], 'my-first'); const planPath = path.join(pkg.getModulePath()!, 'pgpm.plan'); let planContent = fs.readFileSync(planPath, 'utf8'); + expect(planContent).toContain('@v1.0.0'); await pkg.removeFromPlan('schema_myfirstapp'); planContent = fs.readFileSync(planPath, 'utf8'); @@ -179,6 +184,7 @@ describe('Remove Functionality', () => { const pkg = fixture.getModuleProject([], 'my-first'); const planPath = path.join(pkg.getModulePath()!, 'pgpm.plan'); let planContent = fs.readFileSync(planPath, 'utf8'); + expect(planContent).toContain('@v1.0.0'); expect(planContent).toContain('table_users'); expect(planContent).toContain('table_products'); @@ -222,6 +228,7 @@ describe('Remove Functionality', () => { const planPath = path.join(pkg.getModulePath()!, 'pgpm.plan'); const updatedPlan = fs.readFileSync(planPath, 'utf8'); + expect(updatedPlan).not.toContain('schema_myfirstapp'); expect(updatedPlan).not.toContain('table_users'); expect(updatedPlan).not.toContain('table_products'); diff --git a/pgpm/core/__tests__/projects/stage-workspace.test.ts b/pgpm/core/__tests__/projects/stage-workspace.test.ts index f66db24d4..5dd9d5639 100644 --- a/pgpm/core/__tests__/projects/stage-workspace.test.ts +++ b/pgpm/core/__tests__/projects/stage-workspace.test.ts @@ -1,7 +1,7 @@ -import { PgpmPackage, PackageContext } from '../../src/core/class/pgpm'; -import { TestFixture } from '../../test-utils/TestFixture'; -import { CoreDeployTestFixture } from '../../test-utils/CoreDeployTestFixture'; +import { PackageContext,PgpmPackage } from '../../src/core/class/pgpm'; import { TestDatabase } from '../../test-utils'; +import { CoreDeployTestFixture } from '../../test-utils/CoreDeployTestFixture'; +import { TestFixture } from '../../test-utils/TestFixture'; describe('Staging Fixture Tests', () => { let fixture: TestFixture; @@ -31,10 +31,12 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const modules = await project.getModules(); + expect(Array.isArray(modules)).toBe(true); expect(modules.length).toBeGreaterThan(0); const moduleNames = modules.map(m => m.getModuleName()); + expect(moduleNames).toContain('unique-names'); expect(moduleNames.some(name => name.includes('pgpm-uuid'))).toBe(true); }); @@ -46,6 +48,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const availableModules = project.getAvailableModules(); + expect(Array.isArray(availableModules)).toBe(true); expect(availableModules.length).toBeGreaterThan(0); expect(availableModules).toContain('unique-names'); @@ -56,6 +59,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const modules = await project.getModules(); + expect(Array.isArray(modules)).toBe(true); expect(modules.length).toBeGreaterThan(0); @@ -89,6 +93,7 @@ describe('Staging Fixture Tests', () => { modules.forEach(mod => { const moduleName = mod.getModuleName(); const moduleInfo = moduleMap[moduleName]; + expect(moduleInfo).toBeDefined(); expect(moduleInfo.path).toBeTruthy(); }); @@ -108,6 +113,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const name = project.getModuleName(); + expect(typeof name).toBe('string'); expect(name).toBe('unique-names'); }); @@ -122,6 +128,7 @@ describe('Staging Fixture Tests', () => { expect(project.isInModule()).toBe(true); const name = project.getModuleName(); + expect(name).toBe('pgpm-uuid'); }); @@ -130,6 +137,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const info = project.getModuleInfo(); + expect(info.extname).toBeTruthy(); expect(info.version).toMatch(/\d+\.\d+\.\d+/); expect(info.controlFile).toContain('.control'); @@ -142,6 +150,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const deps = project.getRequiredModules(); + expect(Array.isArray(deps)).toBe(true); expect(deps).toContain('plpgsql'); }); @@ -151,6 +160,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const { native, modules: deps } = project.getModuleDependencies('unique-names'); + expect(native).toBeDefined(); expect(deps).toBeDefined(); expect(Array.isArray(deps)).toBe(true); @@ -161,6 +171,7 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const result = await project.getModuleDependencyChanges('unique-names'); + expect(result).toHaveProperty('native'); expect(result).toHaveProperty('modules'); expect(Array.isArray(result.modules)).toBe(true); @@ -184,10 +195,12 @@ describe('Staging Fixture Tests', () => { const project = new PgpmPackage(cwd); const uuidDeps = project.getModuleDependencies('pgpm-uuid'); + expect(uuidDeps.modules).toBeDefined(); expect(Array.isArray(uuidDeps.modules)).toBe(true); const result = await project.getModuleDependencyChanges('pgpm-uuid'); + expect(result.modules).toBeDefined(); expect(Array.isArray(result.modules)).toBe(true); }); @@ -201,6 +214,7 @@ describe('Staging Fixture Tests', () => { expect(result.modules.length).toBeGreaterThan(0); const dependencyNames = result.modules.map(dep => dep.name); + expect(dependencyNames.some(name => name.includes('pgpm-defaults'))).toBe(true); result.modules.forEach(dep => { @@ -243,6 +257,7 @@ describe('Staging Fixture Tests', () => { for (const moduleName of moduleNames) { const deps = project.getModuleDependencies(moduleName); + expect(deps).toHaveProperty('native'); expect(deps).toHaveProperty('modules'); expect(Array.isArray(deps.modules)).toBe(true); diff --git a/pgpm/core/__tests__/projects/tag-functionality.test.ts b/pgpm/core/__tests__/projects/tag-functionality.test.ts index 8ee6b2fb9..287bf35dc 100644 --- a/pgpm/core/__tests__/projects/tag-functionality.test.ts +++ b/pgpm/core/__tests__/projects/tag-functionality.test.ts @@ -1,10 +1,11 @@ process.env.CONSTRUCTIVE_DEBUG = 'true'; -import { TestDatabase } from '../../test-utils'; -import { teardownPgPools } from 'pg-cache'; import { readFileSync } from 'fs'; -import { PgpmPackage } from '../../src/core/class/pgpm'; import { join } from 'path'; +import { teardownPgPools } from 'pg-cache'; + +import { PgpmPackage } from '../../src/core/class/pgpm'; +import { TestDatabase } from '../../test-utils'; import { CoreDeployTestFixture } from '../../test-utils/CoreDeployTestFixture'; describe('Tag functionality with CoreDeployTestFixture', () => { @@ -40,6 +41,7 @@ describe('Tag functionality with CoreDeployTestFixture', () => { const packagePath = join(basePath, 'packages', 'my-first'); const pkg = new PgpmPackage(packagePath); + pkg.addTag('v1.3.0', 'schema_myfirstapp', 'Initial schema tag'); await fixture.deployModule('my-first:@v1.3.0', db.name, ['sqitch', 'simple-w-tags']); @@ -62,6 +64,7 @@ describe('Tag functionality with CoreDeployTestFixture', () => { const packagePath = join(basePath, 'packages', 'my-second'); const pkg = new PgpmPackage(packagePath); + pkg.addTag('v2.2.0', 'create_table', 'Table creation release'); const updatedPlan = readFileSync(join(packagePath, 'pgpm.plan'), 'utf8'); diff --git a/pgpm/core/__tests__/projects/verify-modules.test.ts b/pgpm/core/__tests__/projects/verify-modules.test.ts index c94e64cb5..5bc6ae156 100644 --- a/pgpm/core/__tests__/projects/verify-modules.test.ts +++ b/pgpm/core/__tests__/projects/verify-modules.test.ts @@ -31,19 +31,24 @@ describe('Basic Verification with verifyModules', () => { expect(await db.exists('table', 'mythirdapp.customers')).toBe(true); const schemas = await db.query('SELECT schema_name FROM information_schema.schemata WHERE schema_name IN (\'myfirstapp\', \'mysecondapp\', \'mythirdapp\') ORDER BY schema_name'); + expect(schemas.rows).toHaveLength(3); expect(schemas.rows.map((r: any) => r.schema_name)).toEqual(['myfirstapp', 'mysecondapp', 'mythirdapp']); const tables = await db.query('SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema IN (\'myfirstapp\', \'mysecondapp\', \'mythirdapp\') ORDER BY table_schema, table_name'); + expect(tables.rows).toHaveLength(6); const myfirstappTables = tables.rows.filter((r: any) => r.table_schema === 'myfirstapp').map((r: any) => r.table_name); + expect(myfirstappTables.sort()).toEqual(['products', 'users']); const mysecondappTables = tables.rows.filter((r: any) => r.table_schema === 'mysecondapp').map((r: any) => r.table_name); + expect(mysecondappTables.sort()).toEqual(['consent_agreements', 'user_interactions', 'users']); const mythirdappTables = tables.rows.filter((r: any) => r.table_schema === 'mythirdapp').map((r: any) => r.table_name); + expect(mythirdappTables).toEqual(['customers']); }); }); diff --git a/pgpm/core/__tests__/projects/verify-partial.test.ts b/pgpm/core/__tests__/projects/verify-partial.test.ts index 22842028d..c82c1fbe2 100644 --- a/pgpm/core/__tests__/projects/verify-partial.test.ts +++ b/pgpm/core/__tests__/projects/verify-partial.test.ts @@ -26,6 +26,7 @@ describe('Partial Verification with toChange parameter', () => { "table_name = 'customers'", "table_name = 'nonexistent_table'" ); + fs.writeFileSync(verifyScriptPath, brokenContent); await fixture.verifyModule('my-third:create_schema', db.name, ['sqitch', 'simple-w-tags']); diff --git a/pgpm/core/__tests__/resolution/dependency-resolution-basic.test.ts b/pgpm/core/__tests__/resolution/dependency-resolution-basic.test.ts index d325d3dd2..a84ff18d2 100644 --- a/pgpm/core/__tests__/resolution/dependency-resolution-basic.test.ts +++ b/pgpm/core/__tests__/resolution/dependency-resolution-basic.test.ts @@ -1,5 +1,5 @@ -import { resolveExtensionDependencies, resolveDependencies } from '../../src/resolution/deps'; import { PgpmPackage } from '../../src/core/class/pgpm'; +import { resolveDependencies,resolveExtensionDependencies } from '../../src/resolution/deps'; import { TestFixture } from '../../test-utils'; let fixture: TestFixture; @@ -17,6 +17,7 @@ it('sqitch package dependencies [utils]', async () => { fixture.getFixturePath('constructive', 'packages', 'utils'), 'utils' ); + expect(res).toMatchSnapshot(); }); @@ -25,6 +26,7 @@ it('sqitch package dependencies [simple/1st]', async () => { fixture.getFixturePath('simple', 'packages', 'my-first'), 'my-first' ); + expect(res).toMatchSnapshot(); }); @@ -33,6 +35,7 @@ it('sqitch package dependencies [simple/2nd]', async () => { fixture.getFixturePath('simple', 'packages', 'my-second'), 'my-second' ); + expect(res).toMatchSnapshot(); }); @@ -41,6 +44,7 @@ it('sqitch package dependencies [simple/3rd]', async () => { fixture.getFixturePath('simple', 'packages', 'my-third'), 'my-third' ); + expect(res).toMatchSnapshot(); }); @@ -49,6 +53,7 @@ it('constructive project extensions dependencies', async () => { const modules = pkg.listModules(); const utils = await resolveExtensionDependencies('utils', modules); + expect(utils).toEqual({ external: ['plpgsql', 'uuid-ossp', 'pgcrypto'], resolved: [ @@ -63,6 +68,7 @@ it('constructive project extensions dependencies', async () => { }); const secrets = await resolveExtensionDependencies('secrets', modules); + expect(secrets).toEqual({ external: ['plpgsql', 'uuid-ossp', 'pgcrypto'], resolved: [ @@ -77,6 +83,7 @@ it('constructive project extensions dependencies', async () => { }); const totp = await resolveExtensionDependencies('totp', modules); + expect(totp).toEqual({ external: ['plpgsql', 'uuid-ossp', 'pgcrypto'], resolved: [ diff --git a/pgpm/core/__tests__/resolution/dependency-resolution-internal-tags.test.ts b/pgpm/core/__tests__/resolution/dependency-resolution-internal-tags.test.ts index 67b042d05..3a44ddbb6 100644 --- a/pgpm/core/__tests__/resolution/dependency-resolution-internal-tags.test.ts +++ b/pgpm/core/__tests__/resolution/dependency-resolution-internal-tags.test.ts @@ -18,6 +18,7 @@ it('sqitch package dependencies with internal tag resolution [simple-w-tags/1st] 'my-first', { tagResolution: 'internal' } ); + expect(res).toMatchSnapshot(); }); @@ -27,6 +28,7 @@ it('sqitch package dependencies with internal tag resolution [simple-w-tags/2nd] 'my-second', { tagResolution: 'internal' } ); + expect(res).toMatchSnapshot(); }); @@ -36,6 +38,7 @@ it('sqitch package dependencies with internal tag resolution [simple-w-tags/3rd] 'my-third', { tagResolution: 'internal' } ); + expect(res).toMatchSnapshot(); }); describe('stage fixture dependency resolution - internal tags', () => { @@ -55,6 +58,7 @@ describe('stage fixture dependency resolution - internal tags', () => { tagResolution: 'internal', source: 'plan' }); + expect(res).toMatchSnapshot(); }); @@ -64,6 +68,7 @@ describe('stage fixture dependency resolution - internal tags', () => { tagResolution: 'internal', source: 'sql' }); + expect(res).toMatchSnapshot(); }); }); diff --git a/pgpm/core/__tests__/resolution/dependency-resolution-resolved-tags.test.ts b/pgpm/core/__tests__/resolution/dependency-resolution-resolved-tags.test.ts index ae692b66a..9832f680c 100644 --- a/pgpm/core/__tests__/resolution/dependency-resolution-resolved-tags.test.ts +++ b/pgpm/core/__tests__/resolution/dependency-resolution-resolved-tags.test.ts @@ -18,6 +18,7 @@ it('sqitch package dependencies with resolved tags [simple-w-tags/1st]', async ( 'my-first', { tagResolution: 'resolve' } ); + expect(res).toMatchSnapshot(); }); @@ -27,6 +28,7 @@ it('sqitch package dependencies with resolved tags [simple-w-tags/2nd]', async ( 'my-second', { tagResolution: 'resolve' } ); + expect(res).toMatchSnapshot(); }); @@ -36,6 +38,7 @@ it('sqitch package dependencies with resolved tags [simple-w-tags/3rd]', async ( 'my-third', { tagResolution: 'resolve' } ); + expect(res).toMatchSnapshot(); }); describe('stage fixture dependency resolution - resolved tags', () => { @@ -55,6 +58,7 @@ describe('stage fixture dependency resolution - resolved tags', () => { tagResolution: 'resolve', source: 'plan' }); + expect(res).toMatchSnapshot(); }); @@ -64,6 +68,7 @@ describe('stage fixture dependency resolution - resolved tags', () => { tagResolution: 'resolve', source: 'sql' }); + expect(res).toMatchSnapshot(); }); }); diff --git a/pgpm/core/__tests__/resolution/dependency-resolution-with-tags.test.ts b/pgpm/core/__tests__/resolution/dependency-resolution-with-tags.test.ts index a5460648e..26e0ef51b 100644 --- a/pgpm/core/__tests__/resolution/dependency-resolution-with-tags.test.ts +++ b/pgpm/core/__tests__/resolution/dependency-resolution-with-tags.test.ts @@ -16,6 +16,7 @@ it('sqitch package dependencies [simple-w-tags/1st]', async () => { fixture.getFixturePath('simple-w-tags', 'packages', 'my-first'), 'my-first' ); + expect(res).toMatchSnapshot(); }); @@ -24,6 +25,7 @@ it('sqitch package dependencies [simple-w-tags/2nd]', async () => { fixture.getFixturePath('simple-w-tags', 'packages', 'my-second'), 'my-second' ); + expect(res).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/resolution/header-format-compatibility.test.ts b/pgpm/core/__tests__/resolution/header-format-compatibility.test.ts index fea09ee29..26046aa59 100644 --- a/pgpm/core/__tests__/resolution/header-format-compatibility.test.ts +++ b/pgpm/core/__tests__/resolution/header-format-compatibility.test.ts @@ -1,7 +1,8 @@ +import { mkdirSync,writeFileSync } from 'fs'; +import { join } from 'path'; + import { resolveDependencies } from '../../src/resolution/deps'; import { TestFixture } from '../../test-utils'; -import { writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; describe('Header format compatibility', () => { let fixture: TestFixture; @@ -24,6 +25,7 @@ describe('Header format compatibility', () => { schema 2024-01-01T00:00:00Z Test User # Add schema tables/users [schema] 2024-01-02T00:00:00Z Test User # Add users table `; + writeFileSync(join(moduleDir, 'pgpm.plan'), planContent); if (headerFormat === 'old') { @@ -36,6 +38,7 @@ CREATE SCHEMA app; CREATE TABLE app.users (id serial primary key); `; + writeFileSync(join(moduleDir, 'deploy', 'schema.sql'), schemaContent); mkdirSync(join(moduleDir, 'deploy', 'tables'), { recursive: true }); writeFileSync(join(moduleDir, 'deploy', 'tables', 'users.sql'), usersContent); @@ -49,6 +52,7 @@ CREATE SCHEMA app; CREATE TABLE app.users (id serial primary key); `; + writeFileSync(join(moduleDir, 'deploy', 'schema.sql'), schemaContent); mkdirSync(join(moduleDir, 'deploy', 'tables'), { recursive: true }); writeFileSync(join(moduleDir, 'deploy', 'tables', 'users.sql'), usersContent); @@ -62,6 +66,7 @@ CREATE SCHEMA app; CREATE TABLE app.users (id serial primary key); `; + writeFileSync(join(moduleDir, 'deploy', 'schema.sql'), schemaContent); mkdirSync(join(moduleDir, 'deploy', 'tables'), { recursive: true }); writeFileSync(join(moduleDir, 'deploy', 'tables', 'users.sql'), usersContent); @@ -70,6 +75,7 @@ CREATE TABLE app.users (id serial primary key); test('parses old header format with "to pg"', () => { const moduleDir = join(fixture.tempDir, 'test-old-format'); + setupModule(moduleDir, 'old'); const result = resolveDependencies(moduleDir, 'test-module', { source: 'sql' }); @@ -81,6 +87,7 @@ CREATE TABLE app.users (id serial primary key); test('parses new header format without "to pg"', () => { const moduleDir = join(fixture.tempDir, 'test-new-format'); + setupModule(moduleDir, 'new'); const result = resolveDependencies(moduleDir, 'test-module', { source: 'sql' }); @@ -92,6 +99,7 @@ CREATE TABLE app.users (id serial primary key); test('parses mixed header formats (backwards compatibility)', () => { const moduleDir = join(fixture.tempDir, 'test-mixed-format'); + setupModule(moduleDir, 'mixed'); const result = resolveDependencies(moduleDir, 'test-module', { source: 'sql' }); @@ -103,6 +111,7 @@ CREATE TABLE app.users (id serial primary key); test('validates simple header format without project prefix', () => { const moduleDir = join(fixture.tempDir, 'test-simple-format'); + mkdirSync(join(moduleDir, 'deploy'), { recursive: true }); const planContent = `%syntax-version=1.0.0 @@ -111,12 +120,14 @@ CREATE TABLE app.users (id serial primary key); init 2024-01-01T00:00:00Z Test User # Initialize `; + writeFileSync(join(moduleDir, 'pgpm.plan'), planContent); const initContent = `-- Deploy init SELECT 1; `; + writeFileSync(join(moduleDir, 'deploy', 'init.sql'), initContent); const result = resolveDependencies(moduleDir, 'test-module', { source: 'sql' }); @@ -126,6 +137,7 @@ SELECT 1; test('validates simple header format with old "to pg" suffix', () => { const moduleDir = join(fixture.tempDir, 'test-simple-old-format'); + mkdirSync(join(moduleDir, 'deploy'), { recursive: true }); const planContent = `%syntax-version=1.0.0 @@ -134,12 +146,14 @@ SELECT 1; init 2024-01-01T00:00:00Z Test User # Initialize `; + writeFileSync(join(moduleDir, 'pgpm.plan'), planContent); const initContent = `-- Deploy init to pg SELECT 1; `; + writeFileSync(join(moduleDir, 'deploy', 'init.sql'), initContent); const result = resolveDependencies(moduleDir, 'test-module', { source: 'sql' }); diff --git a/pgpm/core/__tests__/resolution/sql-script-resolution-cross-dependencies.test.ts b/pgpm/core/__tests__/resolution/sql-script-resolution-cross-dependencies.test.ts index d8bf95ede..0d864da41 100644 --- a/pgpm/core/__tests__/resolution/sql-script-resolution-cross-dependencies.test.ts +++ b/pgpm/core/__tests__/resolution/sql-script-resolution-cross-dependencies.test.ts @@ -14,6 +14,7 @@ afterAll(() => { describe('resolve works with cross deps', () => { it('resolves sql in proper order', async () => { const sql = await resolve(fixture.tempFixtureDir); + expect(sql).toBeTruthy(); expect(sql).toMatchSnapshot(); }); diff --git a/pgpm/core/__tests__/resolution/sql-script-resolution-revert.test.ts b/pgpm/core/__tests__/resolution/sql-script-resolution-revert.test.ts index 3f3b4e517..8151566a0 100644 --- a/pgpm/core/__tests__/resolution/sql-script-resolution-revert.test.ts +++ b/pgpm/core/__tests__/resolution/sql-script-resolution-revert.test.ts @@ -24,6 +24,7 @@ commit;`; describe('resolve', () => { it('resolves sql in proper order', async () => { const sql = await resolve(fixture.tempFixtureDir, 'revert'); + expect(sql).toBeTruthy(); expect(sql.trim()).toEqual(expectResult); }); diff --git a/pgpm/core/__tests__/resolution/sql-script-resolution.test.ts b/pgpm/core/__tests__/resolution/sql-script-resolution.test.ts index 1ac2035ce..3eef2724c 100644 --- a/pgpm/core/__tests__/resolution/sql-script-resolution.test.ts +++ b/pgpm/core/__tests__/resolution/sql-script-resolution.test.ts @@ -90,15 +90,18 @@ describe('resolve tests', () => { it('resolves SQL in proper order', async () => { const sql = await resolve(baseFixture.getFixturePath('basic')); + expect(sql).toBeTruthy(); expect(sql.trim()).toBe(expectResult); }); it('resolves SQL in proper order using cwd()', async () => { const originalDir = process.cwd(); + try { process.chdir(baseFixture.getFixturePath('basic')); const sql = await resolve(); + expect(sql).toBeTruthy(); expect(sql.trim()).toBe(expectResult); } finally { @@ -108,9 +111,11 @@ describe('resolve tests', () => { it('resolves SQL in plan order', async () => { const originalDir = process.cwd(); + try { process.chdir(baseFixture.getFixturePath('basic')); const sql = await resolveWithPlan(); + expect(sql).toBeTruthy(); expect(sql.trim()).toBe(expectResult); } finally { diff --git a/pgpm/core/__tests__/roles/roles-sql-generators.test.ts b/pgpm/core/__tests__/roles/roles-sql-generators.test.ts index 47455f0e8..19d376547 100644 --- a/pgpm/core/__tests__/roles/roles-sql-generators.test.ts +++ b/pgpm/core/__tests__/roles/roles-sql-generators.test.ts @@ -1,7 +1,7 @@ import { generateCreateBaseRolesSQL, - generateCreateUserSQL, generateCreateTestUsersSQL, + generateCreateUserSQL, generateRemoveUserSQL } from '../../src/roles'; @@ -52,6 +52,7 @@ describe('Role SQL Generators - Input Validation', () => { authenticated: 'auth', administrator: 'admin' }); + expect(sql).toContain('anon'); expect(sql).toContain('auth'); expect(sql).toContain('admin'); @@ -79,6 +80,7 @@ describe('Role SQL Generators - Input Validation', () => { anonymous: 'anon', authenticated: 'auth' }); + expect(sql).toContain('testuser'); expect(sql).toContain('anon'); expect(sql).toContain('auth'); @@ -127,6 +129,7 @@ describe('Role SQL Generators - Input Validation', () => { it('should generate valid SQL when all parameters are provided', () => { const sql = generateCreateTestUsersSQL(validRoles, validConnections); + expect(sql).toContain('app_user'); expect(sql).toContain('admin_user'); expect(sql).toContain('anon'); @@ -155,6 +158,7 @@ describe('Role SQL Generators - Input Validation', () => { anonymous: 'anon', authenticated: 'auth' }); + expect(sql).toContain('testuser'); expect(sql).toContain('anon'); expect(sql).toContain('auth'); diff --git a/pgpm/core/__tests__/workspace/module-utilities.test.ts b/pgpm/core/__tests__/workspace/module-utilities.test.ts index 736594e02..94a2ca26b 100644 --- a/pgpm/core/__tests__/workspace/module-utilities.test.ts +++ b/pgpm/core/__tests__/workspace/module-utilities.test.ts @@ -1,9 +1,9 @@ +import { PgpmPackage } from '../../src/core/class/pgpm'; import { getExtensionsAndModules, getExtensionsAndModulesChanges, latestChange } from '../../src/modules/modules'; -import { PgpmPackage } from '../../src/core/class/pgpm'; import { TestFixture } from '../../test-utils'; let fixture: TestFixture; @@ -20,6 +20,7 @@ describe('sqitch modules', () => { it('should get modules', async () => { const pkg = new PgpmPackage(fixture.tempFixtureDir); const modules = pkg.listModules(); + expect(modules).toMatchSnapshot(); }); @@ -27,6 +28,7 @@ describe('sqitch modules', () => { const pkg = new PgpmPackage(fixture.tempFixtureDir); const modules = pkg.listModules(); const change = await latestChange('totp', modules, fixture.tempFixtureDir); + expect(change).toBe('procedures/generate_secret'); }); @@ -34,6 +36,7 @@ describe('sqitch modules', () => { const pkg = new PgpmPackage(fixture.tempFixtureDir); const modules = pkg.listModules(); const deps = await getExtensionsAndModules('utils', modules); + expect(deps).toEqual({ native: ['plpgsql', 'uuid-ossp'], sqitch: ['totp', 'pg-verify'], @@ -44,6 +47,7 @@ describe('sqitch modules', () => { const pkg = new PgpmPackage(fixture.tempFixtureDir); const modules = pkg.listModules(); const deps = await getExtensionsAndModulesChanges('utils', modules, fixture.tempFixtureDir); + expect(deps).toMatchSnapshot(); }); }); diff --git a/pgpm/core/src/core/boilerplate-scanner.ts b/pgpm/core/src/core/boilerplate-scanner.ts index dd5326b29..d085d3a93 100644 --- a/pgpm/core/src/core/boilerplate-scanner.ts +++ b/pgpm/core/src/core/boilerplate-scanner.ts @@ -16,14 +16,17 @@ import { */ export function readBoilerplatesConfig(templateDir: string): BoilerplatesRootConfig | null { const configPath = path.join(templateDir, '.boilerplates.json'); + if (fs.existsSync(configPath)) { try { const content = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(content) as BoilerplatesRootConfig; } catch { return null; } } + return null; } @@ -40,6 +43,7 @@ export function readBoilerplateConfig(boilerplatePath: string): BoilerplateConfi if (fs.existsSync(jsonPath)) { try { const content = fs.readFileSync(jsonPath, 'utf-8'); + return JSON.parse(content) as BoilerplateConfig; } catch { return null; @@ -109,9 +113,11 @@ export function findBoilerplateByType( */ export function resolveBoilerplateBaseDir(templateDir: string): string { const rootConfig = readBoilerplatesConfig(templateDir); + if (rootConfig?.dir) { return path.join(templateDir, rootConfig.dir); } + return templateDir; } diff --git a/pgpm/core/src/core/class/pgpm.ts b/pgpm/core/src/core/class/pgpm.ts index 033611dfb..186eeb7ea 100644 --- a/pgpm/core/src/core/class/pgpm.ts +++ b/pgpm/core/src/core/class/pgpm.ts @@ -1,7 +1,6 @@ import { loadConfigSyncFromDir, resolvePgpmPath,walkUp } from '@pgpmjs/env'; import { Logger } from '@pgpmjs/logger'; import { errors, PgpmOptions, PgpmWorkspaceConfig } from '@pgpmjs/types'; -import yanse from 'yanse'; import { execSync } from 'child_process'; import fs from 'fs'; import * as glob from 'glob'; @@ -10,15 +9,10 @@ import { parse } from 'parse-package-name'; import path, { dirname, resolve } from 'path'; import { getPgPool } from 'pg-cache'; import { PgConfig } from 'pg-env'; +import yanse from 'yanse'; -import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TTL_MS, DEFAULT_TEMPLATE_TOOL_NAME, scaffoldTemplate } from '../template-scaffold'; import { getAvailableExtensions } from '../../extensions/extensions'; import { generatePlan, writePlan, writePlanFile } from '../../files'; -import { Tag, ExtendedPlanFile, Change } from '../../files/types'; -import { parsePlanFile } from '../../files/plan/parser'; -import { isValidTagName, isValidChangeName, parseReference } from '../../files/plan/validators'; -import { getNow as getPlanTimestamp } from '../../files/plan/generator'; -import { resolveTagToChangeName } from '../../resolution/resolve'; import { ExtensionInfo, getExtensionInfo, @@ -28,6 +22,11 @@ import { writeExtensions, } from '../../files'; import { generateControlFileContent, writeExtensionMakefile } from '../../files/extension/writer'; +import { getNow as getPlanTimestamp } from '../../files/plan/generator'; +import { parsePlanFile } from '../../files/plan/parser'; +import { isValidChangeName, isValidTagName, parseReference } from '../../files/plan/validators'; +import { Change, Tag } from '../../files/types'; +import { PackageAnalysisIssue, PackageAnalysisResult, RenameOptions } from '../../files/types'; import { PgpmMigrate } from '../../migrate/client'; import { getExtensionsAndModules, @@ -37,23 +36,22 @@ import { ModuleMap } from '../../modules/modules'; import { packageModule } from '../../packaging/package'; -import { resolveExtensionDependencies, resolveDependencies } from '../../resolution/deps'; -import { PackageAnalysisIssue, PackageAnalysisResult, RenameOptions } from '../../files/types'; - +import { resolveDependencies,resolveExtensionDependencies } from '../../resolution/deps'; import { parseTarget } from '../../utils/target-utils'; +import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, DEFAULT_TEMPLATE_TTL_MS, scaffoldTemplate } from '../template-scaffold'; const logger = new Logger('pgpm'); function getUTCTimestamp(d: Date = new Date()): string { return ( - d.getUTCFullYear() + - '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + - '-' + String(d.getUTCDate()).padStart(2, '0') + - 'T' + String(d.getUTCHours()).padStart(2, '0') + - ':' + String(d.getUTCMinutes()).padStart(2, '0') + - ':' + String(d.getUTCSeconds()).padStart(2, '0') + - 'Z' + `${d.getUTCFullYear() + }-${ String(d.getUTCMonth() + 1).padStart(2, '0') + }-${ String(d.getUTCDate()).padStart(2, '0') + }T${ String(d.getUTCHours()).padStart(2, '0') + }:${ String(d.getUTCMinutes()).padStart(2, '0') + }:${ String(d.getUTCSeconds()).padStart(2, '0') + }Z` ); } @@ -157,6 +155,7 @@ export class PgpmPackage { glob.sync(path.join(this.workspacePath!, pattern)) ); const resolvedDirs = dirs.map(dir => path.resolve(dir)); + // Remove duplicates by converting to Set and back to array return [...new Set(resolvedDirs)]; } @@ -166,8 +165,10 @@ export class PgpmPackage { const parentDirs = globs.map(pattern => { // Remove glob characters (*, **, ?, etc.) to get the base path const basePath = pattern.replace(/[*?[\]{}]/g, '').replace(/\/$/, ''); + return path.resolve(this.workspacePath!, basePath); }); + // Remove duplicates by converting to Set and back to array return [...new Set(parentDirs)]; } @@ -178,6 +179,7 @@ export class PgpmPackage { isParentOfAllowedDirs(cwd: string): boolean { const resolvedCwd = path.resolve(cwd); + return this.allowedDirs.some(dir => dir.startsWith(resolvedCwd + path.sep)) || this.allowedParentDirs.some(dir => path.resolve(dir) === resolvedCwd); } @@ -192,6 +194,7 @@ export class PgpmPackage { if (isRoot) { const packagesDir = path.join(this.cwd, 'packages'); + fs.mkdirSync(packagesDir, { recursive: true }); targetPath = path.join(packagesDir, modName); } else if (isParentDir) { @@ -205,6 +208,7 @@ export class PgpmPackage { } fs.mkdirSync(targetPath, { recursive: true }); + return targetPath; } @@ -220,11 +224,13 @@ export class PgpmPackage { if (this.modulePath && this.workspacePath) { const rel = path.relative(this.workspacePath, this.modulePath); const nested = !rel.startsWith('..') && !path.isAbsolute(rel); + return nested ? PackageContext.ModuleInsideWorkspace : PackageContext.Module; } if (this.modulePath) return PackageContext.Module; if (this.workspacePath) return PackageContext.Workspace; + return PackageContext.Outside; } @@ -262,6 +268,7 @@ export class PgpmPackage { for (const dir of dirs) { const proj = new PgpmPackage(dir); + if (proj.isInModule()) { results.push(proj); } @@ -286,6 +293,7 @@ export class PgpmPackage { moduleFiles.forEach((file: string) => { const moduleName = path.basename(file).split('.control')[0]; + if (!filesByName.has(moduleName)) { filesByName.set(moduleName, []); } @@ -294,6 +302,7 @@ export class PgpmPackage { // For each module name, pick the shortest path in case of collisions const selectedFiles = new Map(); + filesByName.forEach((files, moduleName) => { if (files.length === 1) { selectedFiles.set(moduleName, files[0]); @@ -302,6 +311,7 @@ export class PgpmPackage { const shortestFile = files.reduce((shortest, current) => current.length < shortest.length ? current : shortest ); + selectedFiles.set(moduleName, shortestFile); } }); @@ -309,7 +319,9 @@ export class PgpmPackage { // Parse the selected control files return Array.from(selectedFiles.entries()).reduce((acc: ModuleMap, [moduleName, file]) => { const module = parseControlFile(file, this.workspacePath!); + acc[moduleName] = module; + return acc; }, {}); } @@ -319,11 +331,13 @@ export class PgpmPackage { if (this._moduleMap) return this._moduleMap; this._moduleMap = this.listModules(); + return this._moduleMap; } getAvailableModules(): string[] { const modules = this.getModuleMap(); + return getAvailableExtensions(modules); } @@ -335,11 +349,13 @@ export class PgpmPackage { } const modules = this.getModuleMap(); + if (!modules[name]) { throw errors.MODULE_NOT_FOUND({ name }); } const modulePath = path.resolve(this.workspacePath!, modules[name].path); + return new PgpmPackage(modulePath); } @@ -350,17 +366,20 @@ export class PgpmPackage { if (!this._moduleInfo) { this._moduleInfo = getExtensionInfo(this.cwd); } + return this._moduleInfo; } getModuleName(): string { this.ensureModule(); + return getExtensionName(this.cwd); } getRequiredModules(): string[] { this.ensureModule(); const info = this.getModuleInfo(); + return getInstalledExtensions(info.controlFile); } @@ -407,12 +426,15 @@ export class PgpmPackage { uri: modName, entries: [] }); + writePlan(path.join(targetPath, 'pgpm.plan'), plan); // Create deploy, revert, and verify directories const dirs = ['deploy', 'revert', 'verify']; + dirs.forEach(dir => { const dirPath = path.join(targetPath, dir); + if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } @@ -454,11 +476,13 @@ export class PgpmPackage { getLatestChange(moduleName: string): string { const modules = this.getModuleMap(); + return latestChange(moduleName, modules, this.workspacePath!); } getLatestChangeAndVersion(moduleName: string): { change: string; version: string } { const modules = this.getModuleMap(); + return latestChangeAndVersion(moduleName, modules, this.workspacePath!); } @@ -466,12 +490,14 @@ export class PgpmPackage { this.ensureModule(); const moduleName = this.getModuleName(); const moduleMap = this.getModuleMap(); + return resolveExtensionDependencies(moduleName, moduleMap); } getModuleDependencies(moduleName: string): { native: string[]; modules: string[] } { const modules = this.getModuleMap(); const { native, sqitch } = getExtensionsAndModules(moduleName, modules); + return { native, modules: sqitch }; } @@ -481,6 +507,7 @@ export class PgpmPackage { } { const modules = this.getModuleMap(); const { native, sqitch } = getExtensionsAndModulesChanges(moduleName, modules, this.workspacePath!); + return { native, modules: sqitch }; } @@ -489,24 +516,28 @@ export class PgpmPackage { getModulePlan(): string { this.ensureModule(); const planPath = path.join(this.getModulePath()!, 'pgpm.plan'); + return fs.readFileSync(planPath, 'utf8'); } getModuleControlFile(): string { this.ensureModule(); const info = this.getModuleInfo(); + return fs.readFileSync(info.controlFile, 'utf8'); } getModuleMakefile(): string { this.ensureModule(); const info = this.getModuleInfo(); + return fs.readFileSync(info.Makefile, 'utf8'); } getModuleSQL(): string { this.ensureModule(); const info = this.getModuleInfo(); + return fs.readFileSync(info.sqlFile, 'utf8'); } @@ -522,12 +553,14 @@ export class PgpmPackage { // Helper to extract module name from a change reference const getModuleName = (change: string): string | null => { const colonIndex = change.indexOf(':'); + return colonIndex > 0 ? change.substring(0, colonIndex) : null; }; // Helper to determine if a change is truly from an external package const isExternalChange = (change: string): boolean => { const changeModule = getModuleName(change); + return changeModule !== null && changeModule !== moduleName; }; @@ -561,6 +594,7 @@ export class PgpmPackage { Object.keys(deps).forEach(key => { // Normalize the key - strip "/deploy/" and ".sql" if present let normalizedKey = key; + if (normalizedKey.startsWith('/deploy/')) { normalizedKey = normalizedKey.substring(8); // Remove "/deploy/" } @@ -582,6 +616,7 @@ export class PgpmPackage { // Add dependencies, handling both formats const dependencies = deps[key] || []; + dependencies.forEach(dep => { // For truly external dependencies, keep the full reference if (isExternalChange(dep)) { @@ -591,6 +626,7 @@ export class PgpmPackage { } else { // For same-package dependencies, normalize by removing prefix const normalizedDep = normalizeChangeName(dep); + if (!normalizedDeps[standardKey].includes(normalizedDep)) { normalizedDeps[standardKey].push(normalizedDep); } @@ -605,11 +641,13 @@ export class PgpmPackage { // Process external dependencies if needed const includePackages = options.includePackages === true; const preferTags = options.includeTags === true; + if (includePackages && this.workspacePath) { const depData = this.getModuleDependencyChanges(moduleName); if (resolved.length > 0) { const firstKey = `/deploy/${resolved[0]}.sql`; + deps[firstKey] = deps[firstKey] || []; depData.modules.forEach(m => { @@ -625,6 +663,7 @@ export class PgpmPackage { try { const moduleMap = this.getModuleMap(); const modInfo = moduleMap[extModuleName]; + if (modInfo && this.workspacePath) { const planPath = path.join(this.workspacePath, modInfo.path, 'pgpm.plan'); const parsed = parsePlanFile(planPath); @@ -634,6 +673,7 @@ export class PgpmPackage { if (changes.length > 0 && tags.length > 0) { const lastChangeName = changes[changes.length - 1]?.name; const lastTag = tags[tags.length - 1]; + if (lastTag && lastTag.change === lastChangeName) { depToken = `${extModuleName}:@${lastTag.name}`; } @@ -712,6 +752,7 @@ export class PgpmPackage { // Parse existing plan file const planResult = parsePlanFile(planPath); + if (!planResult.data) { throw errors.PLAN_PARSE_ERROR({ planPath, errors: planResult.errors.map(e => e.message).join(', ') }); } @@ -719,6 +760,7 @@ export class PgpmPackage { const plan = planResult.data; let targetChange = changeName; + if (!targetChange) { if (plan.changes.length === 0) { throw new Error('No changes found in plan file. Cannot add tag without a target change.'); @@ -727,6 +769,7 @@ export class PgpmPackage { } else { // Validate that the specified change exists const changeExists = plan.changes.some(c => c.name === targetChange); + if (!changeExists) { throw errors.CHANGE_NOT_FOUND({ change: targetChange }); } @@ -734,6 +777,7 @@ export class PgpmPackage { // Check if tag already exists const existingTag = plan.tags.find(t => t.name === tagName); + if (existingTag) { throw new Error(`Tag '${tagName}' already exists and points to change '${existingTag.change}'.`); } @@ -779,6 +823,7 @@ export class PgpmPackage { } this.addChangeToModule(changeName, dependencies, comment); + return; } @@ -793,6 +838,7 @@ export class PgpmPackage { // Parse existing plan file const planResult = parsePlanFile(planPath); + if (!planResult.data) { throw errors.PLAN_PARSE_ERROR({ planPath, errors: planResult.errors.map(e => e.message).join(', ') }); } @@ -801,6 +847,7 @@ export class PgpmPackage { // Check if change already exists const existingChange = plan.changes.find(c => c.name === changeName); + if (existingChange) { throw new Error(`Change '${changeName}' already exists in plan.`); } @@ -818,6 +865,7 @@ export class PgpmPackage { } const depExists = plan.changes.some(c => c.name === dep); + if (!depExists) { throw new Error(`Dependency '${dep}' not found in plan. Add dependencies before referencing them.`); } @@ -865,6 +913,7 @@ export class PgpmPackage { // Track the relative path from module root const relativePath = path.relative(this.modulePath!, filePath); + createdFiles.push(relativePath); }; @@ -872,7 +921,7 @@ export class PgpmPackage { const deployContent = `-- Deploy: ${changeName} -- made with <3 @ constructive.io -${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join('\n') + '\n' : ''} +${dependencies.length > 0 ? `${dependencies.map(dep => `-- requires: ${dep}`).join('\n') }\n` : ''} -- Add your deployment SQL here `; @@ -922,12 +971,14 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( // Add README file regardless of casing const readmeFile = fs.readdirSync(modPath).find(f => /^readme\.md$/i.test(f)); + if (readmeFile) { files.push(readmeFile); // Include it in the list of files to copy } for (const folder of folders) { const src = path.join(modPath, folder); + if (fs.existsSync(src)) { fs.cpSync(src, path.join(fullDist, folder), { recursive: true }); } @@ -935,6 +986,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( for (const file of files) { const src = path.join(modPath, file); + if (!fs.existsSync(src)) { throw new Error(`Missing required file: ${file}`); } @@ -961,6 +1013,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } const pkgData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); + pkgData.dependencies = pkgData.dependencies || {}; const newlyAdded: string[] = []; @@ -981,6 +1034,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const extDir = dirname(fullConf); const relativeDir = extDir.split('node_modules/')[1]; const dstDir = path.join(skitchExtDir, relativeDir); + return { src: extDir, dst: dstDir, pkg: relativeDir }; }); @@ -994,14 +1048,17 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( logger.success(`βœ” installed ${pkg}`); const pkgJsonFile = path.join(dst, 'package.json'); + if (!fs.existsSync(pkgJsonFile)) { throw new Error(`Missing package.json in installed extension: ${dst}`); } const { version } = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8')); + pkgData.dependencies[name] = `${version}`; const extensionName = getExtensionName(dst); + newlyAdded.push(extensionName); } @@ -1027,6 +1084,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( // ─── Update .control file with actual extension names ────────────── const currentDeps = this.getRequiredModules(); const updatedDeps = Array.from(new Set([...currentDeps, ...newlyAdded])).sort(); + writeExtensions(this.modulePath!, updatedDeps); } @@ -1058,6 +1116,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( for (const moduleName of moduleNames) { const { name } = parse(moduleName); + if (dependencies[name]) { installed.push(name); installedVersions[name] = dependencies[name]; @@ -1095,6 +1154,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( for (const [name, version] of Object.entries(dependencies)) { const extPath = path.join(skitchExtDir, name); + if (fs.existsSync(extPath)) { installed.push(name); installedVersions[name] = version as string; @@ -1141,6 +1201,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( if (modulesToUpdate.length === 0) { logger.info('No modules to update.'); + return { updates: [], affectedModules: [] }; } @@ -1154,11 +1215,13 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const oldVersion = installedVersions[moduleName]; let newVersion: string | null = null; + try { const result = execSync(`npm view ${moduleName} version`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + newVersion = result || null; } catch { logger.warn(`Could not fetch latest version for ${moduleName}`); @@ -1183,6 +1246,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } } const updatesWithChanges = updates.filter(u => u.newVersion && u.newVersion !== u.oldVersion); + return { updates: updatesWithChanges, affectedModules: [] }; } @@ -1192,6 +1256,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( if (modulesToReinstall.length === 0) { logger.success('All modules are already up to date.'); + return { updates, affectedModules: [] }; } @@ -1199,6 +1264,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( await this.installModules(...modulesToReinstall); const { installedVersions: newVersions } = this.getInstalledModules(); + for (const update of updates) { if (modulesToReinstall.includes(update.name)) { update.newVersion = newVersions[update.name] || update.newVersion; @@ -1209,9 +1275,11 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const affectedModules = this.updateWorkspaceModuleVersions( modulesToReinstall.reduce((acc, name) => { const update = updates.find(u => u.name === name); + if (update?.newVersion) { acc[name] = update.newVersion; } + return acc; }, {} as Record) ); @@ -1221,6 +1289,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } logger.success('Upgrade complete.'); + return { updates, affectedModules }; } @@ -1275,9 +1344,11 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( private async getDeployedModules(pgConfig: PgConfig): Promise> { try { const client = new PgpmMigrate(pgConfig); + await client.initialize(); const status = await client.status(); + return new Set(status.map(s => s.package)); } catch (error: any) { if (error.code === '42P01' || error.code === '3F000') { @@ -1313,12 +1384,13 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( // Filter by deployment status if requested if (opts?.filterDeployed && opts?.pgConfig) { const deployedModules = await this.getDeployedModules(opts.pgConfig); + filteredResolved = filteredResolved.filter(module => deployedModules.has(module)); } return { resolved: filteredResolved, - external: external + external }; } @@ -1328,11 +1400,13 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( if (!target) { const context = this.getContext(); + if (context === PackageContext.Module || context === PackageContext.ModuleInsideWorkspace) { name = this.getModuleName(); } else if (context === PackageContext.Workspace) { const modules = this.getModuleMap(); const moduleNames = Object.keys(modules); + if (moduleNames.length === 0) { throw new Error('No modules found in workspace'); } @@ -1342,6 +1416,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } } else { const parsed = parseTarget(target); + name = parsed.packageName; toChange = parsed.toChange; } @@ -1368,6 +1443,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( database: string ): string => { const { host, port, user } = pg ?? {}; + return `${host}:${port}:${user}:${database}:${name}`; }; @@ -1380,22 +1456,26 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( extensions = await this.resolveWorkspaceExtensionDependencies(); } else { const moduleProject = this.getModuleProject(name); + extensions = moduleProject.getModuleExtensions(); } const pgPool = getPgPool(opts.pg); const targetDescription = name === null ? 'all modules' : name; + log.success(`πŸš€ Starting deployment to database ${opts.pg.database}...`); for (const extension of extensions.resolved) { try { if (extensions.external.includes(extension)) { const msg = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`; + log.info(`πŸ“₯ Installing external extension: ${extension}`); await pgPool.query(msg); } else { const modulePath = resolve(this.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Deploying local module: ${extension}`); if (opts.deployment.fast) { @@ -1409,6 +1489,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } let pkg; + try { pkg = await packageModule(localProject.modulePath, { usePlan: opts.deployment.usePlan, @@ -1416,6 +1497,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( }); } catch (err: any) { const errorLines = []; + errorLines.push(`❌ Failed to package module "${extension}" at path: ${modulePath}`); errorLines.push(` Module Path: ${modulePath}`); errorLines.push(` Workspace Path: ${this.workspacePath}`); @@ -1481,6 +1563,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } const moduleProject = this.getModuleProject(name); const modulePath = moduleProject.getModulePath(); + if (!modulePath) { throw errors.PATH_NOT_FOUND({ path: name, type: 'module' }); } @@ -1534,12 +1617,14 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( filterDeployed: true, pgConfig: opts.pg as PgConfig }); + extensionsToRevert = truncateExtensionsToTarget(workspaceExtensions, name); } const pgPool = getPgPool(opts.pg); const targetDescription = name === null ? 'all modules' : name; + log.success(`🧹 Starting revert process on database ${opts.pg.database}...`); const reversedExtensions = [...extensionsToRevert.resolved].reverse(); @@ -1548,6 +1633,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( try { if (extensionsToRevert.external.includes(extension)) { const msg = `DROP EXTENSION IF EXISTS "${extension}" RESTRICT;`; + log.warn(`⚠️ Dropping external extension: ${extension}`); try { await pgPool.query(msg); @@ -1560,6 +1646,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } } else { const modulePath = resolve(this.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Reverting local module: ${extension}`); try { @@ -1595,6 +1682,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } const moduleProject = this.getModuleProject(name); const modulePath = moduleProject.getModulePath(); + if (!modulePath) { throw errors.PATH_NOT_FOUND({ path: name, type: 'module' }); } @@ -1633,22 +1721,26 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( extensions = await this.resolveWorkspaceExtensionDependencies(); } else { const moduleProject = this.getModuleProject(name); + extensions = moduleProject.getModuleExtensions(); } const pgPool = getPgPool(opts.pg); const targetDescription = name === null ? 'all modules' : name; + log.success(`πŸ”Ž Verifying deployment of ${targetDescription} on database ${opts.pg.database}...`); for (const extension of extensions.resolved) { try { if (extensions.external.includes(extension)) { const query = `SELECT 1/count(*) FROM pg_available_extensions WHERE name = $1`; + log.info(`πŸ” Verifying external extension: ${extension}`); await pgPool.query(query, [extension]); } else { const modulePath = resolve(this.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Verifying local module: ${extension}`); try { @@ -1683,6 +1775,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } const moduleProject = this.getModuleProject(name); const modulePath = moduleProject.getModulePath(); + if (!modulePath) { throw errors.PATH_NOT_FOUND({ path: name, type: 'module' }); } @@ -1704,6 +1797,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( async removeFromPlan(toChange: string): Promise { const log = new Logger('remove'); const modulePath = this.getModulePath(); + if (!modulePath) { throw errors.PATH_NOT_FOUND({ path: 'module path', type: 'module' }); } @@ -1720,16 +1814,19 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( if (toChange.startsWith('@')) { const tagName = toChange.substring(1); // Remove the '@' prefix const tagToRemove = plan.tags.find(tag => tag.name === tagName); + if (!tagToRemove) { throw errors.TAG_NOT_FOUND({ tag: toChange }); } const tagChangeIndex = plan.changes.findIndex(c => c.name === tagToRemove.change); + if (tagChangeIndex === -1) { throw errors.CHANGE_NOT_FOUND({ change: tagToRemove.change, plan: `for tag '${toChange}'` }); } const changesToRemove = plan.changes.slice(tagChangeIndex); + plan.changes = plan.changes.slice(0, tagChangeIndex); plan.tags = plan.tags.filter(tag => @@ -1739,6 +1836,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( for (const change of changesToRemove) { for (const scriptType of ['deploy', 'revert', 'verify']) { const scriptPath = path.join(modulePath, scriptType, `${change.name}.sql`); + if (fs.existsSync(scriptPath)) { fs.unlinkSync(scriptPath); log.info(`Deleted ${scriptType}/${change.name}.sql`); @@ -1749,15 +1847,18 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( // Write updated plan file writePlanFile(planPath, plan); log.success(`Removed tag ${toChange} and ${changesToRemove.length} subsequent changes from plan`); + return; } const targetIndex = plan.changes.findIndex(c => c.name === toChange); + if (targetIndex === -1) { throw errors.CHANGE_NOT_FOUND({ change: toChange }); } const changesToRemove = plan.changes.slice(targetIndex); + plan.changes = plan.changes.slice(0, targetIndex); plan.tags = plan.tags.filter(tag => @@ -1767,6 +1868,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( for (const change of changesToRemove) { for (const scriptType of ['deploy', 'revert', 'verify']) { const scriptPath = path.join(modulePath, scriptType, `${change.name}.sql`); + if (fs.existsSync(scriptPath)) { fs.unlinkSync(scriptPath); log.info(`Deleted ${scriptType}/${change.name}.sql`); @@ -1787,28 +1889,38 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const exists = (p: string) => fs.existsSync(p); const read = (p: string) => (exists(p) ? fs.readFileSync(p, 'utf8') : undefined); const planPath = path.join(modPath, 'pgpm.plan'); + if (!exists(planPath)) issues.push({ code: 'missing_plan', message: 'Missing pgpm.plan', file: planPath }); const pkgJsonPath = path.join(modPath, 'package.json'); + if (!exists(pkgJsonPath)) issues.push({ code: 'missing_package_json', message: 'Missing package.json', file: pkgJsonPath }); const makefilePath = info.Makefile; + if (!exists(makefilePath)) issues.push({ code: 'missing_makefile', message: 'Missing Makefile', file: makefilePath }); const controlPath = info.controlFile; + if (!exists(controlPath)) issues.push({ code: 'missing_control', message: 'Missing control file', file: controlPath }); const sqlCombined = info.sqlFile ? path.join(modPath, info.sqlFile) : path.join(modPath, 'sql', `${info.extname}--${info.version}.sql`); + if (!exists(sqlCombined)) issues.push({ code: 'missing_sql', message: 'Missing combined sql file', file: sqlCombined }); const deployDir = path.join(modPath, 'deploy'); + if (!exists(deployDir)) issues.push({ code: 'missing_deploy_dir', message: 'Missing deploy directory', file: deployDir }); const revertDir = path.join(modPath, 'revert'); + if (!exists(revertDir)) issues.push({ code: 'missing_revert_dir', message: 'Missing revert directory', file: revertDir }); const verifyDir = path.join(modPath, 'verify'); + if (!exists(verifyDir)) issues.push({ code: 'missing_verify_dir', message: 'Missing verify directory', file: verifyDir }); if (exists(planPath)) { try { const parsed = parsePlanFile(planPath); const pkgName = parsed.data?.package; + if (!pkgName) issues.push({ code: 'plan_missing_project', message: '%project missing', file: planPath }); if (pkgName && pkgName !== info.extname) issues.push({ code: 'plan_project_mismatch', message: `pgpm.plan %project ${pkgName} != ${info.extname}`, file: planPath }); const uri = parsed.data?.uri; + if (uri && uri !== info.extname) issues.push({ code: 'plan_uri_mismatch', message: `pgpm.plan %uri ${uri} != ${info.extname}`, file: planPath }); } catch (e: any) { issues.push({ code: 'plan_parse_error', message: e?.message || 'Plan parse error', file: planPath }); @@ -1818,17 +1930,21 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const mf = read(makefilePath) || ''; const extMatch = mf.match(/^EXTENSION\s*=\s*(.+)$/m); const dataMatch = mf.match(/^DATA\s*=\s*sql\/(.+)\.sql$/m); + if (!extMatch) issues.push({ code: 'makefile_missing_extension', message: 'Makefile missing EXTENSION', file: makefilePath }); if (!dataMatch) issues.push({ code: 'makefile_missing_data', message: 'Makefile missing DATA', file: makefilePath }); if (extMatch && extMatch[1].trim() !== info.extname) issues.push({ code: 'makefile_extension_mismatch', message: `Makefile EXTENSION ${extMatch[1].trim()} != ${info.extname}`, file: makefilePath }); const expectedData = `${info.extname}--${info.version}`; + if (dataMatch && dataMatch[1].trim() !== expectedData) issues.push({ code: 'makefile_data_mismatch', message: `Makefile DATA sql/${dataMatch[1].trim()}.sql != sql/${expectedData}.sql`, file: makefilePath }); } if (exists(controlPath)) { const base = path.basename(controlPath); const expected = `${info.extname}.control`; + if (base !== expected) issues.push({ code: 'control_filename_mismatch', message: `Control filename ${base} != ${expected}`, file: controlPath }); } + return { ok: issues.length === 0, name: info.extname, path: modPath, issues }; } @@ -1840,13 +1956,16 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( const warnings: string[] = []; const dry = !!opts?.dryRun; const valid = /^[a-z][a-z0-9_]*$/; + if (!valid.test(newName)) { throw errors.INVALID_NAME({ name: newName, type: 'module', rules: 'lowercase letters, digits, underscores; must start with letter' }); } const planPath = path.join(modPath, 'pgpm.plan'); + if (fs.existsSync(planPath)) { try { const parsed = parsePlanFile(planPath); + if (parsed.data) { parsed.data.package = newName; parsed.data.uri = newName; @@ -1860,13 +1979,16 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( warnings.push('missing pgpm.plan'); } const pkgJsonPath = path.join(modPath, 'package.json'); + if (fs.existsSync(pkgJsonPath) && opts?.syncPackageJsonName) { try { const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); const oldName = pkg.name as string | undefined; + if (oldName) { if (oldName.startsWith('@')) { const parts = oldName.split('/'); + if (parts.length === 2) pkg.name = `${parts[0]}/${newName}`; else pkg.name = newName; } else { @@ -1888,15 +2010,19 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( try { const c = fs.readFileSync(oldControl, 'utf8'); const line = c.split('\n').find(l => /^requires/.test(l)); + if (!line) return []; + return line.split('=')[1].split("'")[1].split(',').map(s => s.trim()).filter(Boolean); } catch { return []; } })(); + if (fs.existsSync(oldControl)) { if (!dry) { const content = generateControlFileContent({ name: newName, version, requires }); + fs.writeFileSync(newControl, content); if (oldControl !== newControl && fs.existsSync(oldControl)) fs.rmSync(oldControl); } @@ -1905,6 +2031,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( warnings.push('missing control file'); } const makefilePath = info.Makefile; + if (fs.existsSync(makefilePath)) { if (!dry) writeExtensionMakefile(makefilePath, newName, version); changed.push(makefilePath); @@ -1913,6 +2040,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } const oldSql = path.join(modPath, 'sql', `${info.extname}--${version}.sql`); const newSql = path.join(modPath, 'sql', `${newName}--${version}.sql`); + if (fs.existsSync(oldSql)) { if (!dry) { if (oldSql !== newSql) { @@ -1921,14 +2049,13 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join( } } changed.push(newSql); - } else { - if (fs.existsSync(newSql)) { + } else if (fs.existsSync(newSql)) { changed.push(newSql); } else { warnings.push('missing combined sql file'); } - } this.clearCache(); + return { changed, warnings }; } } diff --git a/pgpm/core/src/core/template-scaffold.ts b/pgpm/core/src/core/template-scaffold.ts index 99ba29f95..dd5a6526b 100644 --- a/pgpm/core/src/core/template-scaffold.ts +++ b/pgpm/core/src/core/template-scaffold.ts @@ -1,14 +1,13 @@ +import { CacheManager, GitCloner, Templatizer } from 'create-gen-app'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { CacheManager, GitCloner, Templatizer } from 'create-gen-app'; -import { BoilerplateQuestion } from './boilerplate-types'; import { readBoilerplateConfig, readBoilerplatesConfig, - resolveBoilerplateBaseDir, } from './boilerplate-scanner'; +import { BoilerplateQuestion } from './boilerplate-types'; export type TemplateKind = 'workspace' | 'module'; @@ -70,6 +69,7 @@ const resolveFromPath = ( const candidateDir = path.isAbsolute(templatePath) ? templatePath : path.join(templateDir, templatePath); + if ( fs.existsSync(candidateDir) && fs.statSync(candidateDir).isDirectory() @@ -79,6 +79,7 @@ const resolveFromPath = ( resolvedTemplatePath: candidateDir, }; } + return { fromPath: templatePath, resolvedTemplatePath: path.join(templateDir, templatePath), @@ -92,6 +93,7 @@ const resolveFromPath = ( if (baseDir) { // New structure: {templateDir}/{baseDir}/{type} const newStructurePath = path.join(templateDir, baseDir, type); + if ( fs.existsSync(newStructurePath) && fs.statSync(newStructurePath).isDirectory() @@ -105,6 +107,7 @@ const resolveFromPath = ( // Fallback to legacy structure: {templateDir}/{type} const legacyPath = path.join(templateDir, type); + if (fs.existsSync(legacyPath) && fs.statSync(legacyPath).isDirectory()) { return { fromPath: type, @@ -188,6 +191,7 @@ export async function scaffoldTemplate( const cacheKey = cacheManager.createKey(normalizedUrl, branch); const expiredMetadata = cacheManager.checkExpiration(cacheKey); + if (expiredMetadata) { cacheManager.clear(cacheKey); } @@ -195,11 +199,13 @@ export async function scaffoldTemplate( let templateDir: string; let cacheUsed = false; const cachedPath = cacheManager.get(cacheKey); + if (cachedPath && !expiredMetadata) { templateDir = cachedPath; cacheUsed = true; } else { const tempDest = path.join(cacheManager.getReposDir(), cacheKey); + gitCloner.clone(normalizedUrl, tempDest, { branch, depth: 1, diff --git a/pgpm/core/src/export/export-meta.ts b/pgpm/core/src/export/export-meta.ts index 60af58835..1845e353d 100644 --- a/pgpm/core/src/export/export-meta.ts +++ b/pgpm/core/src/export/export-meta.ts @@ -230,11 +230,13 @@ export const exportMeta = async ({ opts, dbname, database_id }: ExportMetaParams const sql: Record = {}; const parsers: Record = Object.entries(config).reduce((m, [name, config]) => { m[name] = new Parser(config); + return m; }, {} as Record); const queryAndParse = async (key: string, query: string) => { const result = await pool.query(query, [database_id]); + if (result.rows.length) { sql[key] = await parsers[key].parse(result.rows); } @@ -256,5 +258,5 @@ export const exportMeta = async ({ opts, dbname, database_id }: ExportMetaParams await queryAndParse('rls_module', `SELECT * FROM meta_public.rls_module WHERE database_id = $1`); await queryAndParse('user_auth_module', `SELECT * FROM meta_public.user_auth_module WHERE database_id = $1`); - return Object.entries(sql).reduce((m, [_, v]) => m + '\n\n' + v, ''); + return Object.entries(sql).reduce((m, [_, v]) => `${m }\n\n${ v}`, ''); }; diff --git a/pgpm/core/src/export/export-migrations.ts b/pgpm/core/src/export/export-migrations.ts index ba7055f40..f1815615d 100644 --- a/pgpm/core/src/export/export-migrations.ts +++ b/pgpm/core/src/export/export-migrations.ts @@ -46,7 +46,7 @@ const exportMigrationsToDisk = async ({ extensionName, metaExtensionName }: ExportMigrationsToDiskOptions): Promise => { - outdir = outdir + '/'; + outdir = `${outdir }/`; const pgPool = getPgPool({ ...options.pg, @@ -65,11 +65,13 @@ const exportMigrationsToDisk = async ({ if (!db?.rows?.length) { console.log('NO DATABASES.'); + return; } if (!schemas?.rows?.length) { console.log('NO SCHEMAS.'); + return; } @@ -201,6 +203,7 @@ export const exportMigrations = async ({ }: ExportOptions): Promise => { for (let v = 0; v < dbInfo.database_ids.length; v++) { const databaseId = dbInfo.database_ids[v]; + await exportMigrationsToDisk({ project, options, @@ -251,10 +254,12 @@ const preparePackage = async ({ }: PreparePackageOptions): Promise => { const curDir = process.cwd(); const sqitchDir = path.resolve(path.join(outdir, name)); + mkdirSync(sqitchDir, { recursive: true }); process.chdir(sqitchDir); const plan = glob(path.join(sqitchDir, 'pgpm.plan')); + if (!plan.length) { await project.initModule({ name, @@ -290,6 +295,7 @@ const makeReplacer = ({ schemas, name }: MakeReplacerOptions): ReplacerResult => if (replace[n] && replace[n].length === 2) { return replacer(str.replace(replace[n][0], replace[n][1]), n + 1); } + return str; }; diff --git a/pgpm/core/src/extensions/extensions.ts b/pgpm/core/src/extensions/extensions.ts index e1d97ac28..04e68fde2 100644 --- a/pgpm/core/src/extensions/extensions.ts +++ b/pgpm/core/src/extensions/extensions.ts @@ -30,6 +30,7 @@ export const getAvailableExtensions = ( return Object.keys(modules).reduce((acc, module) => { if (!acc.includes(module)) acc.push(module); + return acc; }, [...coreExtensions]); }; diff --git a/pgpm/core/src/files/extension/reader.ts b/pgpm/core/src/files/extension/reader.ts index 6222edd0b..af92ecf34 100644 --- a/pgpm/core/src/files/extension/reader.ts +++ b/pgpm/core/src/files/extension/reader.ts @@ -82,6 +82,7 @@ export const getInstalledExtensions = (controlFilePath: string): string[] => { } const requiresValue = match[1]; + if (!requiresValue || requiresValue.trim() === '') { return []; } diff --git a/pgpm/core/src/files/extension/writer.ts b/pgpm/core/src/files/extension/writer.ts index 03c70d22c..64fb1ee03 100644 --- a/pgpm/core/src/files/extension/writer.ts +++ b/pgpm/core/src/files/extension/writer.ts @@ -17,6 +17,7 @@ PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) `; + writeFileSync(outputPath, content); }; @@ -86,6 +87,7 @@ export const writeExtensionControlFile = ( version, requires: extensions }); + writeFileSync(outputPath, content); }; @@ -113,6 +115,7 @@ export const writeExtensions = ( // If extInfo is not provided, get it from packageDir const info = extInfo || getExtensionInfo(packageDir); const { controlFile, Makefile, extname, version } = info; + writeExtensionControlFile(controlFile, extname, extensions, version); writeExtensionMakefile(Makefile, extname, version); }; \ No newline at end of file diff --git a/pgpm/core/src/files/plan/generator.ts b/pgpm/core/src/files/plan/generator.ts index be3355edf..a46efdeca 100644 --- a/pgpm/core/src/files/plan/generator.ts +++ b/pgpm/core/src/files/plan/generator.ts @@ -5,13 +5,13 @@ import fs from 'fs'; */ function getUTCTimestamp(d: Date = new Date()): string { return ( - d.getUTCFullYear() + - '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + - '-' + String(d.getUTCDate()).padStart(2, '0') + - 'T' + String(d.getUTCHours()).padStart(2, '0') + - ':' + String(d.getUTCMinutes()).padStart(2, '0') + - ':' + String(d.getUTCSeconds()).padStart(2, '0') + - 'Z' + `${d.getUTCFullYear() + }-${ String(d.getUTCMonth() + 1).padStart(2, '0') + }-${ String(d.getUTCDate()).padStart(2, '0') + }T${ String(d.getUTCHours()).padStart(2, '0') + }:${ String(d.getUTCMinutes()).padStart(2, '0') + }:${ String(d.getUTCSeconds()).padStart(2, '0') + }Z` ); } diff --git a/pgpm/core/src/files/plan/parser.ts b/pgpm/core/src/files/plan/parser.ts index eb1d43f12..da18073e8 100644 --- a/pgpm/core/src/files/plan/parser.ts +++ b/pgpm/core/src/files/plan/parser.ts @@ -1,8 +1,8 @@ +import { errors } from '@pgpmjs/types'; import { readFileSync } from 'fs'; import { Change, ExtendedPlanFile, ParseError, ParseResult,PlanFile, Tag } from '../types'; import { isValidChangeName, isValidDependency, isValidTagName, parseReference } from './validators'; -import { errors } from '@pgpmjs/types'; /** * Parse a Sqitch plan file with full validation @@ -49,6 +49,7 @@ export function parsePlanFile(planPath: string): ParseResult { if (trimmed.startsWith('@')) { const lastChangeName = changes.length > 0 ? changes[changes.length - 1].name : null; const tag = parseTagLine(trimmed, lastChangeName); + if (tag) { if (isValidTagName(tag.name)) { tags.push(tag); @@ -71,6 +72,7 @@ export function parsePlanFile(planPath: string): ParseResult { // Parse change lines const change = parseChangeLine(trimmed); + if (change) { // Validate change name if (!isValidChangeName(change.name)) { @@ -125,6 +127,7 @@ function parseChangeLine(line: string): Change | null { const regex = /^(\S+)(?:\s+\[([^\]]*)\])?(?:\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)(?:\s+([^<]+?))?(?:\s+<([^>]+)>)?(?:\s+#\s+(.*))?)?$/; const match = line.match(regex); + if (!match) { return null; } @@ -181,7 +184,8 @@ function parseTagLine(line: string, lastChangeName: string | null): Tag | null { email: planner, // Shift everything comment: email || comment }; - } else { + } + // Format 2: explicit change name return { name, @@ -191,11 +195,12 @@ function parseTagLine(line: string, lastChangeName: string | null): Tag | null { email, comment }; - } + } // Try simple format without change name const regexSimple = /^(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/; + match = tagContent.match(regexSimple); if (!match) { @@ -224,12 +229,14 @@ export function resolveReference( currentPackage?: string ): { change?: string; tag?: string; error?: string } { const parsed = parseReference(ref); + if (!parsed) { return { error: `Invalid reference: ${ref}` }; } // Handle package qualifier const planPackage = currentPackage || plan.package; + if (parsed.package && parsed.package !== planPackage) { // Cross-package reference - return as-is return { change: ref }; @@ -252,22 +259,26 @@ export function resolveReference( // Handle relative references if (parsed.relative) { const baseResolved = resolveReference(parsed.relative.base, plan, currentPackage); + if (baseResolved.error) { return baseResolved; } // Get the change name from the resolved reference const baseChange = baseResolved.change; + if (!baseChange) { return { error: `Cannot resolve base reference: ${parsed.relative.base}` }; } const changeIndex = plan.changes.findIndex(c => c.name === baseChange); + if (changeIndex === -1) { return { error: `Change not found: ${baseChange}` }; } let targetIndex: number; + if (parsed.relative.direction === '^') { // Go backwards targetIndex = changeIndex - parsed.relative.count; @@ -286,9 +297,11 @@ export function resolveReference( // Handle tag references if (parsed.tag) { const tag = plan.tags.find(t => t.name === parsed.tag); + if (!tag) { return { error: `Tag not found: ${parsed.tag}` }; } + return { tag: parsed.tag, change: tag.change }; } @@ -301,9 +314,11 @@ export function resolveReference( if (parsed.change) { // Validate that the change exists in the plan const changeExists = plan.changes.some(c => c.name === parsed.change); + if (!changeExists) { return { error: `Change not found: ${parsed.change}` }; } + return { change: parsed.change }; } @@ -319,12 +334,14 @@ export function parsePlanFileSimple(planPath: string): PlanFile { if (result.data) { // Return without tags for simple format const { tags, ...planWithoutTags } = result.data; + return planWithoutTags; } // If there are errors, throw with details if (result.errors && result.errors.length > 0) { const errorMessages = result.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n'); + throw errors.PLAN_PARSE_ERROR({ planPath, errors: errorMessages }); } @@ -337,6 +354,7 @@ export function parsePlanFileSimple(planPath: string): PlanFile { */ export function getChanges(planPath: string): string[] { const plan = parsePlanFileSimple(planPath); + return plan.changes.map(change => change.name); } @@ -345,5 +363,6 @@ export function getChanges(planPath: string): string[] { */ export function getLatestChange(planPath: string): string { const changes = getChanges(planPath); + return changes[changes.length - 1] || ''; } diff --git a/pgpm/core/src/files/plan/validators.ts b/pgpm/core/src/files/plan/validators.ts index acf7e7cfa..5c5198c68 100644 --- a/pgpm/core/src/files/plan/validators.ts +++ b/pgpm/core/src/files/plan/validators.ts @@ -9,6 +9,7 @@ function isPunctuation(char: string): boolean { // Common punctuation marks, excluding underscore const punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{|}~]/; + return punctuation.test(char); } @@ -37,12 +38,14 @@ export function isValidChangeName(name: string): boolean { // Check first character - must not be punctuation except underscore const firstChar = name[0]; + if (firstChar !== '_' && isPunctuation(firstChar)) { return false; } // Check last character - must not be punctuation except underscore const lastChar = name[name.length - 1]; + if (lastChar !== '_' && isPunctuation(lastChar)) { return false; } @@ -118,6 +121,7 @@ export function parseReference(ref: string): ParsedReference | null { // Check for package qualifier let workingRef = ref; const colonIndex = ref.indexOf(':'); + if (colonIndex > 0) { const projectPart = ref.substring(0, colonIndex); const remainingPart = ref.substring(colonIndex + 1); @@ -139,71 +143,85 @@ export function parseReference(ref: string): ParsedReference | null { // Check for symbolic references if (workingRef === 'HEAD' || workingRef === '@HEAD') { result.symbolic = 'HEAD'; + return result; } if (workingRef === 'ROOT' || workingRef === '@ROOT') { result.symbolic = 'ROOT'; + return result; } // Check for relative references (e.g., HEAD^, @beta~2, HEAD^^) // Handle multiple ^ characters const multiCaretMatch = workingRef.match(/^(.+?)(\^+)$/); + if (multiCaretMatch) { const [, base, carets] = multiCaretMatch; + result.relative = { - base: base, // Keep the original base including @ prefix + base, // Keep the original base including @ prefix direction: '^', count: carets.length }; + return result; } // Handle ^N or ~N format const relativeMatch = workingRef.match(/^(.+?)([~^])(\d+)$/); + if (relativeMatch) { const [, base, direction, countStr] = relativeMatch; const count = parseInt(countStr, 10); result.relative = { - base: base, // Keep the original base including @ prefix + base, // Keep the original base including @ prefix direction: direction as '^' | '~', count }; + return result; } // Handle single ^ or ~ without number const singleRelativeMatch = workingRef.match(/^(.+?)([~^])$/); + if (singleRelativeMatch) { const [, base, direction] = singleRelativeMatch; result.relative = { - base: base, // Keep the original base including @ prefix + base, // Keep the original base including @ prefix direction: direction as '^' | '~', count: 1 }; + return result; } // Check if it's a SHA1 if (isSHA1(workingRef)) { result.sha1 = workingRef; + return result; } // Check for tag reference (starts with @) if (workingRef.startsWith('@')) { const tagName = workingRef.substring(1); + if (isValidTagName(tagName)) { result.tag = tagName; + return result; } + return null; // Invalid tag name } // Check for change@tag format const atIndex = workingRef.indexOf('@'); + if (atIndex > 0) { const changeName = workingRef.substring(0, atIndex); const tagName = workingRef.substring(atIndex + 1); @@ -211,14 +229,17 @@ export function parseReference(ref: string): ParsedReference | null { if (isValidChangeName(changeName) && isValidTagName(tagName)) { result.change = changeName; result.tag = tagName; + return result; } + return null; // Invalid change or tag name } // Must be a plain change name if (isValidChangeName(workingRef)) { result.change = workingRef; + return result; } diff --git a/pgpm/core/src/files/plan/writer.ts b/pgpm/core/src/files/plan/writer.ts index 47d3b2418..8d2cabbbe 100644 --- a/pgpm/core/src/files/plan/writer.ts +++ b/pgpm/core/src/files/plan/writer.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { Change, PlanFile, SqitchRow, Tag, ExtendedPlanFile } from '../types'; +import { Change, ExtendedPlanFile, SqitchRow, Tag } from '../types'; export interface PlanWriteOptions { outdir: string; @@ -15,6 +15,7 @@ export interface PlanWriteOptions { */ export function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void { const dir = path.resolve(path.join(opts.outdir, opts.name)); + fs.mkdirSync(dir, { recursive: true }); const date = (): string => '2017-08-11T08:11:51Z'; // stubbed timestamp @@ -26,6 +27,7 @@ export function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void // Check if author already contains email in <...> format const emailMatch = authorInput.match(/^(.+?)\s*<([^>]+)>\s*$/); + if (emailMatch) { // Author already has email format: "Name " authorName = emailMatch[1].trim(); @@ -45,7 +47,8 @@ export function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void ${rows .map((row) => { if (duplicates[row.deploy]) { - console.log('DUPLICATE ' + row.deploy); + console.log(`DUPLICATE ${ row.deploy}`); + return ''; } duplicates[row.deploy] = true; @@ -53,6 +56,7 @@ ${rows if (row.deps?.length) { return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${authorName} <${authorEmail}> # add ${row.name}`; } + return `${row.deploy} ${date()} ${authorName} <${authorEmail}> # add ${row.name}`; }) .join('\n')} @@ -66,6 +70,7 @@ ${rows */ export function writePlanFile(planPath: string, plan: ExtendedPlanFile): void { const content = generatePlanFileContent(plan); + fs.writeFileSync(planPath, content); } @@ -76,6 +81,7 @@ export function generatePlanFileContent(plan: ExtendedPlanFile): string { const { package: packageName, uri, changes, tags } = plan; let content = `%syntax-version=1.0.0\n`; + content += `%project=${packageName}\n`; if (uri) { @@ -90,6 +96,7 @@ export function generatePlanFileContent(plan: ExtendedPlanFile): string { content += `\n`; const associatedTags = tags.filter(tag => tag.change === change.name); + for (const tag of associatedTags) { content += generateTagLineContent(tag); content += `\n`; diff --git a/pgpm/core/src/files/sql-scripts/reader.ts b/pgpm/core/src/files/sql-scripts/reader.ts index 18d78bdc4..6e7539bb5 100644 --- a/pgpm/core/src/files/sql-scripts/reader.ts +++ b/pgpm/core/src/files/sql-scripts/reader.ts @@ -19,5 +19,6 @@ export function readScript(basePath: string, scriptType: string, changeName: str */ export function scriptExists(basePath: string, scriptType: string, changeName: string): boolean { const scriptPath = join(basePath, scriptType, `${changeName}.sql`); + return existsSync(scriptPath); } \ No newline at end of file diff --git a/pgpm/core/src/files/sql/writer.ts b/pgpm/core/src/files/sql/writer.ts index 0ff6c24c0..990efa821 100644 --- a/pgpm/core/src/files/sql/writer.ts +++ b/pgpm/core/src/files/sql/writer.ts @@ -26,6 +26,7 @@ export const writeSqitchFiles = (rows: SqitchRow[], opts: SqlWriteOptions): void */ const ordered = (arr?: string[]): string[] => { if (!arr) return []; + return arr.sort((a, b) => a.length - b.length || a.localeCompare(b)); }; @@ -47,6 +48,7 @@ const writeDeploy = (row: SqitchRow, opts: SqlWriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'deploy'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); + fs.mkdirSync(actualDir, { recursive: true }); const sqlContent = opts.replacer(row.content); @@ -64,6 +66,7 @@ ${useTx ? 'BEGIN;' : ''} ${sqlContent} ${useTx ? 'COMMIT;' : ''} `; + fs.writeFileSync(actualFile, content); }; @@ -85,6 +88,7 @@ const writeVerify = (row: SqitchRow, opts: SqlWriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'verify'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); + fs.mkdirSync(actualDir, { recursive: true }); const sqlContent = opts.replacer(row.verify); @@ -96,6 +100,7 @@ ${sqlContent} ${useTx ? 'COMMIT;' : ''} `); + fs.writeFileSync(actualFile, content); }; @@ -117,6 +122,7 @@ const writeRevert = (row: SqitchRow, opts: SqlWriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'revert'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); + fs.mkdirSync(actualDir, { recursive: true }); const sqlContent = opts.replacer(row.revert); @@ -128,5 +134,6 @@ ${sqlContent} ${useTx ? 'COMMIT;' : ''} `; + fs.writeFileSync(actualFile, content); }; diff --git a/pgpm/core/src/index.ts b/pgpm/core/src/index.ts index 1010463fc..5fed75528 100644 --- a/pgpm/core/src/index.ts +++ b/pgpm/core/src/index.ts @@ -1,4 +1,7 @@ +export * from './core/boilerplate-scanner'; +export * from './core/boilerplate-types'; export * from './core/class/pgpm'; +export * from './core/template-scaffold'; export * from './export/export-meta'; export * from './export/export-migrations'; export * from './extensions/extensions'; @@ -9,16 +12,12 @@ export * from './resolution/deps'; export * from './resolution/resolve'; export * from './workspace/paths'; export * from './workspace/utils'; -export * from './core/template-scaffold'; -export * from './core/boilerplate-types'; -export * from './core/boilerplate-scanner'; // Export package-files functionality (now integrated into core) export * from './files'; +export { PgpmInit } from './init/client'; export { cleanSql } from './migrate/clean'; export { PgpmMigrate } from './migrate/client'; -export { PgpmInit } from './init/client'; -export * from './roles'; export { DeployOptions, DeployResult, @@ -31,3 +30,4 @@ export { VerifyResult} from './migrate/types'; export { hashFile, hashString } from './migrate/utils/hash'; export { executeQuery,TransactionContext, TransactionOptions, withTransaction } from './migrate/utils/transaction'; +export * from './roles'; diff --git a/pgpm/core/src/init/client.ts b/pgpm/core/src/init/client.ts index de933356c..384e53608 100644 --- a/pgpm/core/src/init/client.ts +++ b/pgpm/core/src/init/client.ts @@ -3,10 +3,11 @@ import { RoleMapping, TestUserCredentials } from '@pgpmjs/types'; import { Pool } from 'pg'; import { getPgPool } from 'pg-cache'; import { PgConfig } from 'pg-env'; + import { generateCreateBaseRolesSQL, - generateCreateUserSQL, generateCreateTestUsersSQL, + generateCreateUserSQL, generateRemoveUserSQL } from '../roles'; @@ -31,6 +32,7 @@ export class PgpmInit { log.info('Bootstrapping PGPM roles...'); const sql = generateCreateBaseRolesSQL(roles); + await this.pool.query(sql); log.success('Successfully bootstrapped PGPM roles'); @@ -52,6 +54,7 @@ export class PgpmInit { log.info('Bootstrapping PGPM test roles...'); const sql = generateCreateTestUsersSQL(roles, connections); + await this.pool.query(sql); log.success('Successfully bootstrapped PGPM test roles'); @@ -79,6 +82,7 @@ export class PgpmInit { log.info(`Bootstrapping PGPM database roles for user: ${username}...`); const sql = generateCreateUserSQL(username, password, roles, useLocksForRoles); + await this.pool.query(sql); log.success(`Successfully bootstrapped PGPM database roles for user: ${username}`); @@ -104,6 +108,7 @@ export class PgpmInit { log.info(`Removing PGPM database roles for user: ${username}...`); const sql = generateRemoveUserSQL(username, roles, useLocksForRoles); + await this.pool.query(sql); log.success(`Successfully removed PGPM database roles for user: ${username}`); diff --git a/pgpm/core/src/migrate/clean.ts b/pgpm/core/src/migrate/clean.ts index 35b641de2..6e0de1920 100644 --- a/pgpm/core/src/migrate/clean.ts +++ b/pgpm/core/src/migrate/clean.ts @@ -4,11 +4,13 @@ import { deparse,parse } from 'pgsql-parser'; const filterStatements = (stmts: RawStmt[]): { filteredStmts: RawStmt[], hasFiltered: boolean } => { const filteredStmts = stmts.filter(node => { const stmt = node.stmt; + return stmt && !stmt.hasOwnProperty('TransactionStmt') && !stmt.hasOwnProperty('CreateExtensionStmt'); }); const hasFiltered = filteredStmts.length !== stmts.length; + return { filteredStmts, hasFiltered }; }; diff --git a/pgpm/core/src/migrate/client.ts b/pgpm/core/src/migrate/client.ts index 6ba5adaf8..ccbc46702 100644 --- a/pgpm/core/src/migrate/client.ts +++ b/pgpm/core/src/migrate/client.ts @@ -25,6 +25,7 @@ import { executeQuery, withTransaction } from './utils/transaction'; // Helper function to get changes in order function getChangesInOrder(planPath: string, reverse: boolean = false): Change[] { const plan = parsePlanFileSimple(planPath); + return reverse ? [...plan.changes].reverse() : plan.changes; } @@ -51,6 +52,7 @@ export class PgpmMigrate { private toUnqualifiedLocal(pkg: string, nm: string) { if (!nm.includes(':')) return nm; const [p, local] = nm.split(':', 2); + if (p === pkg) return local; throw new Error(`Cross-package change encountered in local tracking: ${nm} (current package: ${pkg})`); } @@ -59,6 +61,7 @@ export class PgpmMigrate { this.pgConfig = config; // Use environment variable DEPLOYMENT_HASH_METHOD if available, otherwise use options or default to 'content' const envHashMethod = process.env.DEPLOYMENT_HASH_METHOD as HashMethod; + this.hashMethod = options.hashMethod || envHashMethod || 'content'; this.pool = getPgPool(this.pgConfig); this.eventLogger = new EventLogger(this.pgConfig); @@ -70,9 +73,10 @@ export class PgpmMigrate { private async calculateScriptHash(filePath: string): Promise { if (this.hashMethod === 'ast') { return await hashSqlFile(filePath); - } else { - return await hashFile(filePath); } + + return await hashFile(filePath); + } /** @@ -160,15 +164,18 @@ export class PgpmMigrate { } const isDeployed = await this.isDeployed(plan.package, change.name); + if (isDeployed) { log.info(`Skipping already deployed change: ${change.name}`); const unqualified = this.toUnqualifiedLocal(plan.package, change.name); + skipped.push(unqualified); continue; } // Read deploy script const deployScript = readScript(dirname(planPath), 'deploy', change.name); + if (!deployScript) { log.error(`Deploy script not found for change: ${change.name}`); failed = change.name; @@ -203,6 +210,7 @@ export class PgpmMigrate { ); const unqualified = this.toUnqualifiedLocal(plan.package, change.name); + deployed.push(unqualified); log.success(`Successfully ${logOnly ? 'logged' : 'deployed'}: ${change.name}`); } catch (error: any) { @@ -217,6 +225,7 @@ export class PgpmMigrate { // Build comprehensive error message const errorLines = []; + errorLines.push(`Failed to deploy ${change.name}:`); errorLines.push(` Change: ${change.name}`); errorLines.push(` Package: ${plan.package}`); @@ -306,15 +315,18 @@ export class PgpmMigrate { // Check if deployed const isDeployed = await this.isDeployed(plan.package, change.name); + if (!isDeployed) { log.info(`Skipping not deployed change: ${change.name}`); const unqualified = this.toUnqualifiedLocal(plan.package, change.name); + skipped.push(unqualified); continue; } // Read revert script const revertScript = readScript(dirname(planPath), 'revert', change.name); + if (!revertScript) { log.error(`Revert script not found for change: ${change.name}`); failed = change.name; @@ -380,12 +392,14 @@ export class PgpmMigrate { // Check if deployed const isDeployed = await this.isDeployed(plan.package, change.name); + if (!isDeployed) { continue; } // Read verify script const verifyScript = readScript(dirname(planPath), 'verify', change.name); + if (!verifyScript) { log.warn(`Verify script not found for change: ${change.name}`); continue; @@ -405,6 +419,7 @@ export class PgpmMigrate { log.success(`Successfully verified: ${change.name}`); } else { const verificationError = new Error(`Verification failed for ${change.name}`) as any; + verificationError.code = 'VERIFICATION_FAILED'; throw verificationError; } @@ -474,6 +489,7 @@ export class PgpmMigrate { AND table_name IN ('projects', 'changes', 'tags', 'events') ) `); + return result.rows[0].exists; } @@ -493,6 +509,7 @@ export class PgpmMigrate { if (!schemaResult.rows[0].exists) { log.info('No Sqitch schema found, nothing to import'); + return; } @@ -596,6 +613,7 @@ export class PgpmMigrate { `, [plan.package]); const deployedSet = new Set(deployedResult.rows.map((r: any) => r.change_name)); + return allChanges.filter(c => !deployedSet.has(c.name)).map(c => c.name); } catch (error: any) { // If schema doesn't exist, all changes are pending @@ -654,6 +672,7 @@ export class PgpmMigrate { return result.rows.map(row => row.requires); } catch (error) { log.error(`Failed to get dependencies for ${packageName}:${changeName}:`, error); + return []; } } diff --git a/pgpm/core/src/migrate/utils/hash.ts b/pgpm/core/src/migrate/utils/hash.ts index 7fdd545b3..46db75751 100644 --- a/pgpm/core/src/migrate/utils/hash.ts +++ b/pgpm/core/src/migrate/utils/hash.ts @@ -9,6 +9,7 @@ import { cleanTree } from '../../packaging/package'; */ export async function hashFile(filePath: string): Promise { const content = await readFile(filePath, 'utf-8'); + return createHash('sha256').update(content).digest('hex'); } @@ -27,5 +28,6 @@ export async function hashSqlFile(filePath: string): Promise { const parsed = await parse(content); const cleaned = cleanTree(parsed); const astString = JSON.stringify(cleaned); + return hashString(astString); } diff --git a/pgpm/core/src/migrate/utils/transaction.ts b/pgpm/core/src/migrate/utils/transaction.ts index 6d385f4fa..4e8340917 100644 --- a/pgpm/core/src/migrate/utils/transaction.ts +++ b/pgpm/core/src/migrate/utils/transaction.ts @@ -47,26 +47,31 @@ export async function withTransaction( if (!options.useTransaction) { // No transaction - use pool directly log.debug('Executing without transaction'); + return fn({ client: pool, isTransaction: false, queryHistory, addQuery }); } // Use transaction const client = await pool.connect(); const transactionStartTime = Date.now(); + log.debug('Starting transaction'); try { const beginTime = Date.now(); + await client.query('BEGIN'); addQuery('BEGIN', [], beginTime); const result = await fn({ client, isTransaction: true, queryHistory, addQuery }); const commitTime = Date.now(); + await client.query('COMMIT'); addQuery('COMMIT', [], commitTime); const transactionDuration = Date.now() - transactionStartTime; + log.debug(`Transaction committed successfully in ${transactionDuration}ms`); return result; @@ -84,6 +89,7 @@ export async function withTransaction( // Enhanced error logging with context const errorLines = []; + errorLines.push(`Transaction rolled back due to error after ${transactionDuration}ms:`); errorLines.push(`Error Code: ${error.code || 'N/A'}`); errorLines.push(`Error Message: ${error.message || 'N/A'}`); @@ -96,6 +102,7 @@ export async function withTransaction( const params = entry.params && entry.params.length > 0 ? ` with params: ${JSON.stringify(entry.params.slice(0, 2))}${entry.params.length > 2 ? '...' : ''}` : ''; + errorLines.push(` ${index + 1}. ${entry.query.split('\n')[0].trim()}${params}${duration}`); }); } @@ -147,6 +154,7 @@ export async function executeQuery( // Enhanced error logging const errorLines = []; + errorLines.push(`Query failed after ${duration}ms:`); errorLines.push(` Query: ${query.split('\n')[0].trim()}`); if (params && params.length > 0) { diff --git a/pgpm/core/src/modules/modules.ts b/pgpm/core/src/modules/modules.ts index 642e017b6..68363e2b9 100644 --- a/pgpm/core/src/modules/modules.ts +++ b/pgpm/core/src/modules/modules.ts @@ -1,6 +1,7 @@ -import { getLatestChange, Module } from '../files'; import { errors } from '@pgpmjs/types'; +import { getLatestChange, Module } from '../files'; + export type ModuleMap = Record; /** @@ -12,11 +13,13 @@ export const latestChange = ( basePath: string ): string => { const module = modules[sqlmodule]; + if (!module) { throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); } const planPath = `${basePath}/${module.path}/pgpm.plan`; + return getLatestChange(planPath); }; @@ -29,6 +32,7 @@ export const latestChangeAndVersion = ( basePath: string ): { change: string; version: string } => { const module = modules[sqlmodule]; + if (!module) { throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); } @@ -48,6 +52,7 @@ export const getExtensionsAndModules = ( modules: ModuleMap ): { native: string[]; sqitch: string[] } => { const module = modules[sqlmodule]; + if (!module) { throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); } @@ -78,6 +83,7 @@ export const getExtensionsAndModulesChanges = ( const sqitchWithDetails = sqitch.map( (mod) => { const { change, version } = latestChangeAndVersion(mod, modules, basePath); + return { name: mod, latest: change, version }; }); diff --git a/pgpm/core/src/packaging/package.ts b/pgpm/core/src/packaging/package.ts index a361f3159..56ce89f03 100644 --- a/pgpm/core/src/packaging/package.ts +++ b/pgpm/core/src/packaging/package.ts @@ -35,8 +35,10 @@ interface WritePackageOptions extends PackageModuleOptions { const filterStatements = (stmts: RawStmt[], extension: boolean): RawStmt[] => { if (!extension) return stmts; + return stmts.filter(node => { const stmt = node.stmt; + return !stmt.hasOwnProperty('TransactionStmt') && !stmt.hasOwnProperty('CreateExtensionStmt'); }); @@ -51,6 +53,7 @@ export const packageModule = async ( if (!sql?.trim()) { log.warn(`⚠️ No SQL generated for module at ${packageDir}. Skipping.`); + return { sql: '' }; } @@ -58,6 +61,7 @@ export const packageModule = async ( try { const parsed = await parse(sql); + parsed.stmts = filterStatements(parsed.stmts as any, extension); const topLine = extension @@ -83,6 +87,7 @@ export const packageModule = async ( const diff = JSON.stringify(cleanTree(tree1)) !== JSON.stringify(cleanTree(tree2)); + if (diff) { results.diff = true; results.tree1 = JSON.stringify(cleanTree(tree1), null, 2); @@ -136,6 +141,7 @@ export const writePackage = async ({ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); const regex = new RegExp(`${extname}--[0-9.]+.sql`); + writeFileSync(makePath, Makefile.replace(regex, sqlFileName)); } @@ -147,6 +153,7 @@ export const writePackage = async ({ } const writePath = `${outPath}/${sqlFileName}`; + writeFileSync(writePath, sql); log.success(`${relative(packageDir, writePath)} written`); }; diff --git a/pgpm/core/src/packaging/transform.ts b/pgpm/core/src/packaging/transform.ts index d77c6df20..476d61419 100644 --- a/pgpm/core/src/packaging/transform.ts +++ b/pgpm/core/src/packaging/transform.ts @@ -22,6 +22,7 @@ export const transformProps = (obj: any, props: TransformProps): any => { if (obj instanceof Date) { copy = new Date(); copy.setTime(obj.getTime()); + return copy; } @@ -31,6 +32,7 @@ export const transformProps = (obj: any, props: TransformProps): any => { for (let i = 0, len = obj.length; i < len; i++) { copy[i] = transformProps(obj[i], props); } + return copy; } @@ -41,6 +43,7 @@ export const transformProps = (obj: any, props: TransformProps): any => { if (Object.prototype.hasOwnProperty.call(obj, attr)) { if (props.hasOwnProperty(attr)) { const propRule = props[attr]; + if (typeof propRule === 'function') { // Apply function transformation copy[attr] = propRule(obj[attr]); @@ -57,6 +60,7 @@ export const transformProps = (obj: any, props: TransformProps): any => { } } } + return copy; } @@ -72,6 +76,8 @@ export const transformProps = (obj: any, props: TransformProps): any => { */ export const transform = async (statement: string, props: TransformProps): Promise => { let tree = await parse(statement); + tree = transformProps(tree, props); + return await deparse(tree as any); }; diff --git a/pgpm/core/src/projects/deploy.ts b/pgpm/core/src/projects/deploy.ts index 448df75d7..693317007 100644 --- a/pgpm/core/src/projects/deploy.ts +++ b/pgpm/core/src/projects/deploy.ts @@ -24,6 +24,7 @@ const getCacheKey = ( database: string ): string => { const { host, port, user } = pg ?? {}; + return `${host}:${port}:${user}:${database}:${name}`; }; @@ -37,6 +38,7 @@ export const deployProject = async ( toChange?: string ): Promise => { const mergedOpts = getEnvOptions(opts); + log.info(`πŸ” Gathering modules from ${pkg.workspacePath}...`); const modules = pkg.getModuleMap(); @@ -59,11 +61,13 @@ export const deployProject = async ( try { if (extensions.external.includes(extension)) { const msg = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`; + log.info(`πŸ“₯ Installing external extension: ${extension}`); log.debug(`> ${msg}`); await pgPool.query(msg); } else { const modulePath = resolve(pkg.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Deploying local module: ${extension}`); log.debug(`β†’ Path: ${modulePath}`); @@ -79,6 +83,7 @@ export const deployProject = async ( } let modulePackage; + try { modulePackage = await packageModule(localProject.modulePath, { usePlan: mergedOpts.deployment.usePlan, @@ -87,6 +92,7 @@ export const deployProject = async ( } catch (err: any) { // Build comprehensive error message const errorLines = []; + errorLines.push(`❌ Failed to package module "${extension}" at path: ${modulePath}`); errorLines.push(` Module Path: ${modulePath}`); errorLines.push(` Workspace Path: ${pkg.workspacePath}`); @@ -152,5 +158,6 @@ export const deployProject = async ( } log.success(`βœ… Deployment complete for ${name}.`); + return extensions; }; diff --git a/pgpm/core/src/projects/revert.ts b/pgpm/core/src/projects/revert.ts index e5f471c79..555a9c4fd 100644 --- a/pgpm/core/src/projects/revert.ts +++ b/pgpm/core/src/projects/revert.ts @@ -56,6 +56,7 @@ export const revertProject = async ( try { if (extensions.external.includes(extension)) { const msg = `DROP EXTENSION IF EXISTS "${extension}" RESTRICT;`; + log.warn(`⚠️ Dropping external extension: ${extension}`); log.debug(`> ${msg}`); try { @@ -69,6 +70,7 @@ export const revertProject = async ( } } else { const modulePath = resolve(pkg.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Reverting local module: ${extension}`); log.debug(`β†’ Path: ${modulePath}`); @@ -100,5 +102,6 @@ export const revertProject = async ( } log.success(`βœ… Revert complete for ${name}.`); + return extensions; }; diff --git a/pgpm/core/src/projects/verify.ts b/pgpm/core/src/projects/verify.ts index ac9e9a8a7..2a2e82937 100644 --- a/pgpm/core/src/projects/verify.ts +++ b/pgpm/core/src/projects/verify.ts @@ -48,11 +48,13 @@ export const verifyProject = async ( try { if (extensions.external.includes(extension)) { const query = `SELECT 1/count(*) FROM pg_available_extensions WHERE name = $1`; + log.info(`πŸ” Verifying external extension: ${extension}`); log.debug(`> ${query}`); await pgPool.query(query, [extension]); } else { const modulePath = resolve(pkg.workspacePath!, modules[extension].path); + log.info(`πŸ“‚ Verifying local module: ${extension}`); log.debug(`β†’ Path: ${modulePath}`); log.debug(`β†’ Command: constructive migrate verify db:pg:${database}`); @@ -80,5 +82,6 @@ export const verifyProject = async ( } log.success(`βœ… Verification complete for ${name}.`); + return extensions; }; diff --git a/pgpm/core/src/resolution/deps.ts b/pgpm/core/src/resolution/deps.ts index 5e44922bf..ab3ce8465 100644 --- a/pgpm/core/src/resolution/deps.ts +++ b/pgpm/core/src/resolution/deps.ts @@ -1,3 +1,4 @@ +import { errors } from '@pgpmjs/types'; import { readFileSync } from 'fs'; import { sync as glob } from 'glob'; import { join,relative } from 'path'; @@ -5,7 +6,6 @@ import { join,relative } from 'path'; import { PgpmPackage } from '../core/class/pgpm'; import { parsePlanFile } from '../files/plan/parser'; import { ExtendedPlanFile } from '../files/types'; -import { errors } from '@pgpmjs/types'; /** * Represents a dependency graph where keys are module identifiers @@ -103,6 +103,7 @@ function createDependencyResolver( if (transformModule) { const result = transformModule(sqlmodule, extname); + moduleToResolve = result.module; edges = result.edges; returnEarly = result.returnEarly || false; @@ -122,7 +123,9 @@ function createDependencyResolver( if (returnEarly) { const index = unresolved.indexOf(sqlmodule); + unresolved.splice(index, 1); + return; } @@ -138,6 +141,7 @@ function createDependencyResolver( resolved.push(moduleToResolve); const index = unresolved.indexOf(sqlmodule); + unresolved.splice(index, 1); }; } @@ -169,6 +173,7 @@ export const resolveExtensionDependencies = ( const external: string[] = []; const deps: DependencyGraph = Object.keys(modules).reduce((memo, key) => { memo[key] = modules[key].requires; + return memo; }, {} as DependencyGraph); @@ -270,6 +275,7 @@ export const resolveDependencies = ( try { let planPath: string; + if (packageName === extname) { // For the current package planPath = join(packageDir, 'pgpm.plan'); @@ -284,6 +290,7 @@ export const resolveDependencies = ( } const workspacePath = project.getWorkspacePath(); + if (!workspacePath) { throw new Error(`No workspace found for module ${packageName}`); } @@ -292,8 +299,10 @@ export const resolveDependencies = ( } const result = parsePlanFile(planPath); + if (result.data) { planCache[packageName] = result.data; + return result.data; } } catch (error) { @@ -312,14 +321,18 @@ export const resolveDependencies = ( // - Internal refs like "extname:change" are normalized to "change". const resolveTagToChange = (projectName: string, tagName: string): string | null => { const plan = loadPlanFile(projectName); + if (!plan) return null; const tag = plan.tags.find(t => t.name === tagName); + if (!tag) return null; + return tag.change; }; if (source === 'plan') { const plan = loadPlanFile(extname); + if (!plan) { throw errors.PLAN_PARSE_ERROR({ planPath: `${extname}/pgpm.plan`, errors: 'Plan file not found or failed to parse while using plan-only resolution' }); } @@ -331,39 +344,49 @@ export const resolveDependencies = ( const normalizeInternal = (dep: string): string => { if (/:/.test(dep)) { const [project, localKey] = dep.split(':', 2); + if (project === extname) return localKey; } + return dep; }; const resolveTagDep = (projectName: string, tagName: string): string | null => { const change = resolveTagToChange(projectName, tagName); + if (!change) return null; + return `${projectName}:${change}`; }; for (const ch of plan.changes) { const key = makeKey(ch.name); + deps[key] = []; const changeDeps: string[] = (ch as any).dependencies || []; + for (const rawDep of changeDeps) { let dep = rawDep.trim(); if (dep.includes('@')) { const m = dep.match(/^([^:]+):@(.+)$/); + if (m) { const projectName = m[1]; const tagName = m[2]; const resolved = resolveTagDep(projectName, tagName); + if (resolved) { if (tagResolution === 'resolve') dep = resolved; else if (tagResolution === 'internal') tagMappings[dep] = resolved; } } else { const m2 = dep.match(/^@(.+)$/); + if (m2) { const tagName = m2[1]; const resolved = resolveTagDep(extname, tagName); + if (resolved) { if (tagResolution === 'resolve') dep = resolved; else if (tagResolution === 'internal') tagMappings[dep] = resolved; @@ -374,6 +397,7 @@ export const resolveDependencies = ( if (/:/.test(dep)) { const [project] = dep.split(':', 2); + if (project !== extname) { external.push(dep); if (!deps[dep]) deps[dep] = []; @@ -394,8 +418,10 @@ export const resolveDependencies = ( if (tagResolution === 'preserve') { let moduleToResolve = sqlmodule; let edges = deps[makeKey(sqlmodule)]; + if (/:/.test(sqlmodule)) { const [project, localKey] = sqlmodule.split(':', 2); + if (project === extnameLocal) { moduleToResolve = localKey; edges = deps[makeKey(localKey)]; @@ -403,52 +429,58 @@ export const resolveDependencies = ( } else { external.push(sqlmodule); deps[sqlmodule] = deps[sqlmodule] || []; + return { module: sqlmodule, edges: [], returnEarly: true }; } - } else { - if (!edges) throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); - } + } else if (!edges) throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); + return { module: moduleToResolve, edges }; } if (/:/.test(originalModule)) { const [project] = originalModule.split(':', 2); + if (project !== extnameLocal) { external.push(originalModule); deps[originalModule] = deps[originalModule] || []; + return { module: originalModule, edges: [], returnEarly: true }; } } let moduleToResolve = sqlmodule; + if (tagResolution === 'internal' && tagMappings[sqlmodule]) { moduleToResolve = tagMappings[sqlmodule]; } let edges = deps[makeKey(moduleToResolve)]; + if (/:/.test(moduleToResolve)) { const [project, localKey] = moduleToResolve.split(':', 2); + if (project === extnameLocal) { moduleToResolve = localKey; edges = deps[makeKey(localKey)]; if (!edges) throw errors.MODULE_NOT_FOUND({ name: `${localKey} (from ${project}:${localKey})` }); } - } else { - if (!edges) { + } else if (!edges) { edges = deps[makeKey(sqlmodule)]; if (!edges) throw errors.MODULE_NOT_FOUND({ name: sqlmodule }); } - } if (tagResolution === 'internal' && edges) { const processedEdges = edges.map(dep => { if (/:/.test(dep)) { const [project] = dep.split(':', 2); + if (project !== extnameLocal) return dep; } if (tagMappings[dep]) return tagMappings[dep]; + return dep; }); + return { module: moduleToResolve, edges: processedEdges }; } @@ -471,12 +503,14 @@ export const resolveDependencies = ( for (const file of files) { const data = readFileSync(file, 'utf-8'); const lines = data.split('\n'); - const key = '/' + relative(packageDir, file); + const key = `/${ relative(packageDir, file)}`; + deps[key] = []; for (const line of lines) { // Handle requires statements const requiresMatch = line.match(/^-- requires: (.*)/); + if (requiresMatch) { const dep = requiresMatch[1].trim(); @@ -489,6 +523,7 @@ export const resolveDependencies = ( // For other modes, handle tag resolution if (dep.includes('@')) { const match = dep.match(/^([^:]+):@(.+)$/); + if (match) { const [, projectName, tagName] = match; const taggedChange = resolveTagToChange(projectName, tagName); @@ -497,6 +532,7 @@ export const resolveDependencies = ( if (tagResolution === 'resolve') { // Full resolution: replace tag with actual change const resolvedDep = `${projectName}:${taggedChange}`; + deps[key].push(resolvedDep); } else if (tagResolution === 'internal') { // Internal resolution: keep tag in deps but track mapping @@ -526,6 +562,7 @@ export const resolveDependencies = ( m2 = line.match(/^-- Deploy ([^:]*):([\w\/]+)(?:\s+to\s+pg)?/); if (m2) { const actualProject = m2[1]; + keyToTest = m2[2]; if (extname !== actualProject) { @@ -538,6 +575,7 @@ export const resolveDependencies = ( } const expectedKey = makeKey(keyToTest); + if (key !== expectedKey) { throw new Error( `Deployment script path or internal name mismatch: @@ -554,7 +592,7 @@ export const resolveDependencies = ( keyToTest = m2[1].trim(); if (key !== makeKey(keyToTest)) { throw new Error( - 'deployment script in wrong place or is named wrong internally\n' + line + `deployment script in wrong place or is named wrong internally\n${ line}` ); } } @@ -573,6 +611,7 @@ export const resolveDependencies = ( if (/:/.test(sqlmodule)) { // Has a prefix β€” could be internal or external const [project, localKey] = sqlmodule.split(':', 2); + if (project === extname) { // Internal reference to current package moduleToResolve = localKey; @@ -584,6 +623,7 @@ export const resolveDependencies = ( // External reference β€” always OK, even if not in deps yet external.push(sqlmodule); deps[sqlmodule] = []; + return { module: sqlmodule, edges: [], returnEarly: true }; } } else { @@ -599,16 +639,19 @@ export const resolveDependencies = ( // Check if the ORIGINAL module (before tag resolution) is external if (/:/.test(originalModule)) { const [project, localKey] = originalModule.split(':', 2); + if (project !== extname) { // External reference β€” always OK, even if not in deps yet external.push(originalModule); deps[originalModule] = deps[originalModule] || []; + return { module: originalModule, edges: [], returnEarly: true }; } } // For internal resolution mode, check if this module is a tag and resolve it let moduleToResolve = sqlmodule; + if (tagResolution === 'internal' && tagMappings[sqlmodule]) { moduleToResolve = tagMappings[sqlmodule]; } @@ -618,6 +661,7 @@ export const resolveDependencies = ( if (/:/.test(moduleToResolve)) { // Has a prefix β€” must be internal since we already handled external above const [project, localKey] = moduleToResolve.split(':', 2); + if (project === extname) { // Internal reference to current package moduleToResolve = localKey; @@ -643,6 +687,7 @@ export const resolveDependencies = ( // Check if this dependency is external - if so, don't resolve tags if (/:/.test(dep)) { const [project, localKey] = dep.split(':', 2); + if (project !== extname) { // External dependency - keep original tag name return dep; @@ -653,8 +698,10 @@ export const resolveDependencies = ( if (tagMappings[dep]) { return tagMappings[dep]; } + return dep; }); + return { module: moduleToResolve, edges: processedEdges }; } @@ -682,11 +729,13 @@ export const resolveDependencies = ( dep_resolve('_virtual/app', resolved, unresolved); const index = resolved.indexOf('_virtual/app'); + resolved.splice(index, 1); delete deps[makeKey('_virtual/app')]; const extensions = resolved.filter((module) => module.startsWith('extensions/')); const normalSql = resolved.filter((module) => !module.startsWith('extensions/')); + resolved = [...extensions, ...normalSql]; return { external, resolved, deps, resolvedTags: tagMappings }; diff --git a/pgpm/core/src/resolution/resolve.ts b/pgpm/core/src/resolution/resolve.ts index 504f9416c..fe9a3331b 100644 --- a/pgpm/core/src/resolution/resolve.ts +++ b/pgpm/core/src/resolution/resolve.ts @@ -1,9 +1,9 @@ +import { errors } from '@pgpmjs/types'; import { readFileSync } from 'fs'; import { getChanges, getExtensionName } from '../files'; import { parsePlanFile } from '../files/plan/parser'; import { resolveDependencies } from './deps'; -import { errors } from '@pgpmjs/types'; /** * Resolves SQL scripts for deployment or reversion. @@ -26,6 +26,7 @@ export const resolve = ( if (external.includes(script)) continue; const file = `${pkgDir}/${scriptType}/${script}.sql`; const dscript = readFileSync(file, 'utf-8'); + sqlfile.push(dscript); } @@ -55,6 +56,7 @@ export const resolveWithPlan = ( for (const script of resolved) { const file = `${pkgDir}/${scriptType}/${script}.sql`; const dscript = readFileSync(file, 'utf-8'); + sqlfile.push(dscript); } @@ -95,6 +97,7 @@ export const resolveTagToChangeName = ( if (tagReference.startsWith('@') && !tagReference.includes(':')) { if (!currentProject) { const plan = parsePlanFile(planPath); + if (!plan.data) { throw errors.PLAN_PARSE_ERROR({ planPath, errors: 'Could not parse plan file' }); } @@ -105,6 +108,7 @@ export const resolveTagToChangeName = ( // Parse package:@tagName format const match = tagReference.match(/^([^:]+):@(.+)$/); + if (!match) { throw errors.INVALID_NAME({ name: tagReference, type: 'tag', rules: 'Expected format: package:@tagName or @tagName' }); } @@ -120,6 +124,7 @@ export const resolveTagToChangeName = ( // Find the tag in the plan const tag = planResult.data.tags?.find((t: any) => t.name === tagName); + if (!tag) { throw errors.TAG_NOT_FOUND({ tag: tagName, project: projectName }); } diff --git a/pgpm/core/src/utils/target-utils.ts b/pgpm/core/src/utils/target-utils.ts index a4c07ec84..0dd768e7a 100644 --- a/pgpm/core/src/utils/target-utils.ts +++ b/pgpm/core/src/utils/target-utils.ts @@ -24,6 +24,7 @@ export function parseTarget(target: string): ParsedTarget { if (!beforeAt) { throw errors.INVALID_NAME({ name: target, type: 'tag', rules: 'Expected format: package:@tagName' }); } + return { packageName: beforeAt, toChange: `@${afterAt}` }; } diff --git a/pgpm/core/src/workspace/utils.ts b/pgpm/core/src/workspace/utils.ts index f0bda19c0..2f10ad78c 100644 --- a/pgpm/core/src/workspace/utils.ts +++ b/pgpm/core/src/workspace/utils.ts @@ -1,6 +1,6 @@ +import { errors } from '@pgpmjs/types'; import { existsSync } from 'fs'; import { dirname, resolve } from 'path'; -import { errors } from '@pgpmjs/types'; /** * Recursively walks up directories to find a specific file (sync version). @@ -13,11 +13,13 @@ export const walkUp = (startDir: string, filename: string): string => { while (currentDir) { const targetPath = resolve(currentDir, filename); + if (existsSync(targetPath)) { return currentDir; } const parentDir = dirname(currentDir); + if (parentDir === currentDir) { break; } diff --git a/pgpm/core/test-utils/CoreDeployTestFixture.ts b/pgpm/core/test-utils/CoreDeployTestFixture.ts index 20a4571fc..5687c8d9b 100644 --- a/pgpm/core/test-utils/CoreDeployTestFixture.ts +++ b/pgpm/core/test-utils/CoreDeployTestFixture.ts @@ -22,7 +22,9 @@ export class CoreDeployTestFixture extends TestFixture { } const db = await this.migrateFixture.setupTestDatabase(); + this.databases.push(db); + return db; } diff --git a/pgpm/core/test-utils/MigrateTestFixture.ts b/pgpm/core/test-utils/MigrateTestFixture.ts index c878560ad..9fd6ff15f 100644 --- a/pgpm/core/test-utils/MigrateTestFixture.ts +++ b/pgpm/core/test-utils/MigrateTestFixture.ts @@ -27,6 +27,7 @@ export class MigrateTestFixture { // Create database using admin pool const adminPool = getPgPool(baseConfig); + try { await adminPool.query(`CREATE DATABASE "${dbName}"`); } catch (e) { @@ -55,10 +56,12 @@ export class MigrateTestFixture { // Initialize migrate schema const migrate = new PgpmMigrate(config); + await migrate.initialize(); // Get pool for test database operations const pool = getPgPool(pgConfig); + this.pools.push(pool); const fixture = this; @@ -79,8 +82,9 @@ export class MigrateTestFixture { ) as exists`, [name] ); + return result.rows[0].exists; - } else { + } const [schema, table] = name.includes('.') ? name.split('.') : ['public', name]; const result = await pool.query( `SELECT EXISTS ( @@ -89,8 +93,9 @@ export class MigrateTestFixture { ) as exists`, [schema, table] ); + return result.rows[0].exists; - } + }, async getDeployedChanges() { @@ -99,6 +104,7 @@ export class MigrateTestFixture { FROM pgpm_migrate.changes ORDER BY deployed_at` ); + return result.rows; }, @@ -137,6 +143,7 @@ export class MigrateTestFixture { WHERE c.package = $1 AND c.change_name = $2`, [packageName, changeName] ); + return result.rows.map((row: any) => row.requires); }, @@ -147,6 +154,7 @@ export class MigrateTestFixture { }; this.databases.push(db); + return db; } @@ -165,6 +173,7 @@ export class MigrateTestFixture { createPlanFile(packageName: string, changes: MigrateTestChange[]): string { const tempDir = mkdtempSync(join(tmpdir(), 'migrate-test-')); + this.tempDirs.push(tempDir); const lines = [ @@ -193,6 +202,7 @@ export class MigrateTestFixture { } const planPath = join(tempDir, 'pgpm.plan'); + writeFileSync(planPath, lines.join('\n')); return tempDir; @@ -200,6 +210,7 @@ export class MigrateTestFixture { createScript(dir: string, type: 'deploy' | 'revert' | 'verify', name: string, content: string): void { const scriptDir = join(dir, type); + mkdirSync(scriptDir, { recursive: true }); writeFileSync(join(scriptDir, `${name}.sql`), content); } diff --git a/pgpm/core/test-utils/TestFixture.ts b/pgpm/core/test-utils/TestFixture.ts index 1c4107ec7..c8807d1ed 100644 --- a/pgpm/core/test-utils/TestFixture.ts +++ b/pgpm/core/test-utils/TestFixture.ts @@ -15,6 +15,7 @@ export class TestFixture { constructor(...fixturePath: string[]) { const originalFixtureDir = getFixturePath(...fixturePath); + this.tempDir = mkdtempSync(path.join(os.tmpdir(), 'constructive-io-graphql-test-')); this.tempFixtureDir = path.join(this.tempDir, ...fixturePath); @@ -27,7 +28,9 @@ export class TestFixture { const workspace = new PgpmPackage(this.getFixturePath(...workspacePath)); const moduleMap = workspace.getModuleMap(); const meta = moduleMap[moduleName]; + if (!meta) throw new Error(`Module ${moduleName} not found in workspace`); + return new PgpmPackage(this.getFixturePath(...workspacePath, meta.path)); }; } diff --git a/pgpm/core/test-utils/TestPlan.ts b/pgpm/core/test-utils/TestPlan.ts index 25a59f813..56a6cc985 100644 --- a/pgpm/core/test-utils/TestPlan.ts +++ b/pgpm/core/test-utils/TestPlan.ts @@ -12,6 +12,7 @@ export class TestPlan { // Otherwise, resolve relative to root __fixtures__/sqitch-plans const rootDir = dirname(dirname(dirname(__dirname))); // Go up to workspace root const basePath = join(rootDir, '__fixtures__', 'sqitch-plans'); + this.fixturePath = join(basePath, fixturePath); if (!existsSync(this.fixturePath)) { diff --git a/pgpm/core/test-utils/utils/index.ts b/pgpm/core/test-utils/utils/index.ts index 5e1360167..a73ef963b 100644 --- a/pgpm/core/test-utils/utils/index.ts +++ b/pgpm/core/test-utils/utils/index.ts @@ -14,6 +14,7 @@ export const cleanText = (text: string): string => export async function teardownAllPools(): Promise { const { teardownPgPools } = await import('pg-cache'); + await teardownPgPools(); } diff --git a/pgpm/env/__tests__/merge.test.ts b/pgpm/env/__tests__/merge.test.ts index 2409a578a..e645e6903 100644 --- a/pgpm/env/__tests__/merge.test.ts +++ b/pgpm/env/__tests__/merge.test.ts @@ -1,6 +1,7 @@ -import { getConnEnvOptions } from '../src/merge'; import { pgpmDefaults } from '@pgpmjs/types'; +import { getConnEnvOptions } from '../src/merge'; + describe('getConnEnvOptions', () => { describe('roles resolution', () => { it('should always return roles with default values when no overrides provided', () => { diff --git a/pgpm/env/src/config.ts b/pgpm/env/src/config.ts index 686a92837..0aadb5cd5 100644 --- a/pgpm/env/src/config.ts +++ b/pgpm/env/src/config.ts @@ -1,6 +1,7 @@ +import { PgpmOptions } from '@pgpmjs/types'; import * as fs from 'fs'; import * as path from 'path'; -import { PgpmOptions } from '@pgpmjs/types'; + import { walkUp } from './utils'; /** @@ -17,6 +18,7 @@ export const loadConfigFileSync = (configPath: string): PgpmOptions => { case '.js': // delete require.cache[require.resolve(configPath)]; const configModule = require(configPath); + return configModule.default || configModule; default: @@ -36,12 +38,13 @@ export const loadConfigSyncFromDir = (dir: string): PgpmOptions => { for (const filename of configFiles) { const configPath = path.join(dir, filename); + if (fs.existsSync(configPath)) { return loadConfigFileSync(configPath); } } - throw new Error('No pgpm config file found. Expected one of: ' + configFiles.join(', ')); + throw new Error(`No pgpm config file found. Expected one of: ${ configFiles.join(', ')}`); }; /** @@ -54,6 +57,7 @@ export const loadConfigSync = (cwd: string = process.cwd()): PgpmOptions => { for (const filename of configFiles) { try { const configDir = walkUp(cwd, filename); + return loadConfigSyncFromDir(configDir); } catch { } diff --git a/pgpm/env/src/env.ts b/pgpm/env/src/env.ts index e73162533..a6c615990 100644 --- a/pgpm/env/src/env.ts +++ b/pgpm/env/src/env.ts @@ -2,16 +2,19 @@ import { PgpmOptions } from '@pgpmjs/types'; const parseEnvNumber = (val?: string): number | undefined => { const num = Number(val); + return !isNaN(num) ? num : undefined; }; export const parseEnvBoolean = (val?: string): boolean | undefined => { if (val === undefined) return undefined; + return ['true', '1', 'yes'].includes(val.toLowerCase()); }; const parseEnvStringArray = (val?: string): string[] | undefined => { if (!val) return undefined; + return val .split(',') .map(s => s.trim()) @@ -188,6 +191,8 @@ type NodeEnv = 'development' | 'production' | 'test'; export const getNodeEnv = (): NodeEnv => { const env = process.env.NODE_ENV?.toLowerCase(); + if (env === 'production' || env === 'test') return env; + return 'development'; }; diff --git a/pgpm/env/src/index.ts b/pgpm/env/src/index.ts index 8fc394671..784810bb6 100644 --- a/pgpm/env/src/index.ts +++ b/pgpm/env/src/index.ts @@ -1,6 +1,5 @@ -export { getEnvOptions, getConnEnvOptions, getDeploymentEnvOptions } from './merge'; -export { loadConfigSync, loadConfigSyncFromDir, loadConfigFileSync, resolvePgpmPath } from './config'; +export { loadConfigFileSync, loadConfigSync, loadConfigSyncFromDir, resolvePgpmPath } from './config'; export { getEnvVars, getNodeEnv, parseEnvBoolean } from './env'; +export { getConnEnvOptions, getDeploymentEnvOptions,getEnvOptions } from './merge'; export { walkUp } from './utils'; - -export type { PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; +export type { DeploymentOptions,PgpmOptions, PgTestConnectionOptions } from '@pgpmjs/types'; diff --git a/pgpm/env/src/merge.ts b/pgpm/env/src/merge.ts index 2f19791a9..df397dba7 100644 --- a/pgpm/env/src/merge.ts +++ b/pgpm/env/src/merge.ts @@ -1,5 +1,6 @@ +import { DeploymentOptions,pgpmDefaults, PgpmOptions, PgTestConnectionOptions } from '@pgpmjs/types'; import deepmerge from 'deepmerge'; -import { pgpmDefaults, PgpmOptions, PgTestConnectionOptions, DeploymentOptions } from '@pgpmjs/types'; + import { loadConfigSync } from './config'; import { getEnvVars } from './env'; @@ -52,5 +53,6 @@ export const getDeploymentEnvOptions = (overrides: Partial = const opts = getEnvOptions({ deployment: overrides }, cwd); + return opts.deployment; }; diff --git a/pgpm/env/src/utils.ts b/pgpm/env/src/utils.ts index 77a66df62..08c9c37d2 100644 --- a/pgpm/env/src/utils.ts +++ b/pgpm/env/src/utils.ts @@ -12,11 +12,13 @@ export const walkUp = (startDir: string, filename: string): string => { while (currentDir) { const targetPath = resolve(currentDir, filename); + if (existsSync(targetPath)) { return currentDir; } const parentDir = dirname(currentDir); + if (parentDir === currentDir) { break; } diff --git a/pgpm/logger/src/logger.ts b/pgpm/logger/src/logger.ts index f24cf2585..ced818961 100644 --- a/pgpm/logger/src/logger.ts +++ b/pgpm/logger/src/logger.ts @@ -72,6 +72,7 @@ let { include: allowedScopes, exclude: blockedScopes } = parseScopeFilter(proces // Update scopes at runtime export const setLogScopes = (scopes: string[]) => { const parsed = parseScopeFilter(scopes.join(',')); + allowedScopes = parsed.include; blockedScopes = parsed.exclude; }; @@ -110,9 +111,9 @@ export class Logger { const outputParts = showTimestamp ? [yanse.dim(`[${new Date().toISOString()}]`), tag, prefix, ...formattedArgs] : [tag, prefix, ...formattedArgs]; - const output = outputParts + const output = `${outputParts .map(arg => String(arg)) - .join(' ') + '\n'; + .join(' ') }\n`; stream.write(output); } diff --git a/pgpm/types/src/error-factory.ts b/pgpm/types/src/error-factory.ts index 117505c52..a160d11be 100644 --- a/pgpm/types/src/error-factory.ts +++ b/pgpm/types/src/error-factory.ts @@ -12,6 +12,7 @@ export const makeError = ( overrideMessage?: string ): PgpmError => { const message = overrideMessage || messageFn(context); + return new PgpmError(code, message, context, httpCode); }; }; diff --git a/pgpm/types/src/index.ts b/pgpm/types/src/index.ts index 767794b62..003725216 100644 --- a/pgpm/types/src/index.ts +++ b/pgpm/types/src/index.ts @@ -1,5 +1,5 @@ export * from './error'; export * from './error-factory'; -export * from './pgpm'; export * from './jobs'; +export * from './pgpm'; export * from './update'; diff --git a/pgpm/types/src/pgpm.ts b/pgpm/types/src/pgpm.ts index 1b701e6ae..f5f345b26 100644 --- a/pgpm/types/src/pgpm.ts +++ b/pgpm/types/src/pgpm.ts @@ -1,5 +1,6 @@ import { execSync } from 'child_process'; import { PgConfig } from 'pg-env'; + import { JobsConfig } from './jobs'; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18d936e91..17e38c8f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 '@jest/test-sequencer': specifier: ^30.2.0 version: 30.2.0 diff --git a/postgres/drizzle-orm-test/__tests__/drizzle-proxy.test.ts b/postgres/drizzle-orm-test/__tests__/drizzle-proxy.test.ts index fc3c86222..d722b38e0 100644 --- a/postgres/drizzle-orm-test/__tests__/drizzle-proxy.test.ts +++ b/postgres/drizzle-orm-test/__tests__/drizzle-proxy.test.ts @@ -90,6 +90,7 @@ describe('drizzle-orm-test', () => { ]); const result = await drizzleDb.select().from(users); + // Should see 2 new records plus 1 seeded record for user 1 expect(result).toHaveLength(3); }); @@ -107,6 +108,7 @@ describe('drizzle-orm-test', () => { const result = await drizzleDb.select().from(users); const updated = result.find(r => r.name === 'Alice Updated'); + expect(updated).toBeDefined(); expect(updated!.name).toBe('Alice Updated'); }); @@ -125,6 +127,7 @@ describe('drizzle-orm-test', () => { ]); let result = await drizzleDb.select().from(users); + expect(result).toHaveLength(3); await drizzleDb.delete(users).where(eq(users.name, 'Alice')); @@ -181,11 +184,13 @@ describe('drizzle-orm-test', () => { const result1 = await drizzleDb.execute<{ value: string }>( `SELECT current_setting('app.user_id', true) as value` ); + expect(result1.rows[0].value).toBe('123'); const result2 = await drizzleDb.execute<{ value: string }>( `SELECT current_setting('app.user_id', true) as value` ); + expect(result2.rows[0].value).toBe('123'); }); }); @@ -202,6 +207,7 @@ describe('drizzle-orm-test', () => { await drizzleDb.insert(users).values({ name: 'Test User', userId: '999' }); const result = await drizzleDb.select().from(users); + expect(result.length).toBeGreaterThan(0); }); @@ -214,6 +220,7 @@ describe('drizzle-orm-test', () => { const drizzleDb = drizzle(db.client); const result = await drizzleDb.select().from(users); + expect(result).toHaveLength(0); }); }); @@ -273,6 +280,7 @@ describe('drizzle-orm-test', () => { await drizzleDb.insert(users).values({ name: 'New User 1 Record', userId: '1' }); const rows = await drizzleDb.select().from(users); + expect(rows).toHaveLength(2); expect(rows.every(r => r.userId === '1')).toBe(true); }); @@ -290,6 +298,7 @@ describe('drizzle-orm-test', () => { .where(eq(users.userId, '1')); const rows = await drizzleDb.select().from(users); + expect(rows).toHaveLength(1); expect(rows[0].name).toBe('Updated User 1 Record'); }); @@ -305,6 +314,7 @@ describe('drizzle-orm-test', () => { await drizzleDb.insert(users).values({ name: 'To Delete', userId: '1' }); let rows = await drizzleDb.select().from(users); + expect(rows).toHaveLength(2); await drizzleDb.delete(users).where(eq(users.name, 'To Delete')); @@ -324,6 +334,7 @@ describe('drizzle-orm-test', () => { }); let rows = await drizzleDb.select().from(users); + expect(rows).toHaveLength(1); expect(rows[0].userId).toBe('1'); diff --git a/postgres/drizzle-orm-test/__tests__/drizzle-seed.test.ts b/postgres/drizzle-orm-test/__tests__/drizzle-seed.test.ts index 2c448e66e..1318634f6 100644 --- a/postgres/drizzle-orm-test/__tests__/drizzle-seed.test.ts +++ b/postgres/drizzle-orm-test/__tests__/drizzle-seed.test.ts @@ -1,11 +1,12 @@ process.env.LOG_SCOPE = 'drizzle-orm-test'; -import { writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; -import { tmpdir } from 'os'; -import { drizzle } from 'drizzle-orm/node-postgres'; -import { pgTable, pgSchema, uuid, text, serial, integer } from 'drizzle-orm/pg-core'; import { eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import {pgSchema, serial, text, uuid } from 'drizzle-orm/pg-core'; +import { mkdirSync,writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; import { seed } from 'pgsql-test'; + import { getConnections, PgTestClient } from '../src'; let pg: PgTestClient; @@ -163,6 +164,7 @@ describe('loadJson() method with Drizzle', () => { }); const result = await drizzleDb.select().from(usersTable); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Alice'); }); @@ -171,7 +173,8 @@ describe('loadJson() method with Drizzle', () => { describe('loadCsv() method with Drizzle', () => { it('should load users from CSV with pg client and query via Drizzle', async () => { const csvPath = join(testDir, 'users.csv'); - const csvContent = 'id,name\n' + [users[0], users[1]].map(u => `${u.id},${u.name}`).join('\n') + '\n'; + const csvContent = `id,name\n${ [users[0], users[1]].map(u => `${u.id},${u.name}`).join('\n') }\n`; + writeFileSync(csvPath, csvContent); await pg.loadCsv({ @@ -192,10 +195,12 @@ describe('loadCsv() method with Drizzle', () => { const csvPath = join(testDir, 'users2.csv'); const csvContent = 'id,name\n' + `${users[2].id},${users[2].name}\n`; + writeFileSync(csvPath, csvContent); // Use savepoint to handle expected failure without aborting transaction const savepointName = 'csv_rls_test'; + await db.savepoint(savepointName); await expect( @@ -213,7 +218,7 @@ describe('loadCsv() method with Drizzle', () => { const petsPath = join(testDir, 'pets.csv'); writeFileSync(usersPath, `id,name\n${users[0].id},${users[0].name}\n`); - writeFileSync(petsPath, 'id,owner_id,name\n' + [pets[0], pets[1]].map(p => `${p.id},${p.owner_id},${p.name}`).join('\n') + '\n'); + writeFileSync(petsPath, `id,owner_id,name\n${ [pets[0], pets[1]].map(p => `${p.id},${p.owner_id},${p.name}`).join('\n') }\n`); await pg.loadCsv({ 'custom.users': usersPath, @@ -240,6 +245,7 @@ describe('loadSql() method with Drizzle', () => { const sqlContent = [users[0], users[1]] .map(u => `INSERT INTO custom.users (id, name) VALUES ('${u.id}', '${u.name}');`) .join('\n'); + writeFileSync(sqlPath, sqlContent); await pg.loadSql([sqlPath]); @@ -257,6 +263,7 @@ describe('loadSql() method with Drizzle', () => { await db.beforeEach(); const sqlPath = join(testDir, 'seed2.sql'); + writeFileSync(sqlPath, `INSERT INTO custom.users (id, name) VALUES ('${users[2].id}', '${users[2].name}');`); await db.loadSql([sqlPath]); @@ -276,6 +283,7 @@ describe('loadSql() method with Drizzle', () => { const petsSql = [pets[0], pets[1]] .map(p => `INSERT INTO custom.pets (id, owner_id, name) VALUES (${p.id}, '${p.owner_id}', '${p.name}');`) .join('\n'); + writeFileSync(petsPath, petsSql); await pg.loadSql([usersPath, petsPath]); @@ -392,6 +400,7 @@ describe('Drizzle insert/update/delete with seed data', () => { }); const result = await drizzleDb.select().from(usersTable); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Bob'); }); @@ -399,6 +408,7 @@ describe('Drizzle insert/update/delete with seed data', () => { it('should seed with loadCsv and then update via Drizzle', async () => { const csvPath = join(testDir, 'users-update.csv'); const csvContent = `id,name\n${users[0].id},${users[0].name}\n`; + writeFileSync(csvPath, csvContent); await pg.loadCsv({ @@ -412,12 +422,14 @@ describe('Drizzle insert/update/delete with seed data', () => { .where(eq(usersTable.id, users[0].id)); const result = await drizzleDb.select().from(usersTable); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Alice Updated'); }); it('should seed with loadSql and then delete via Drizzle', async () => { const sqlPath = join(testDir, 'seed-delete.sql'); + writeFileSync(sqlPath, `INSERT INTO custom.users (id, name) VALUES ('${users[0].id}', '${users[0].name}'), ('${users[1].id}', '${users[1].name}');`); await pg.loadSql([sqlPath]); @@ -427,6 +439,7 @@ describe('Drizzle insert/update/delete with seed data', () => { await drizzleDb.delete(usersTable).where(eq(usersTable.id, users[0].id)); const result = await drizzleDb.select().from(usersTable); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Bob'); }); diff --git a/postgres/drizzle-orm-test/src/proxy-client.ts b/postgres/drizzle-orm-test/src/proxy-client.ts index 9db719584..8b4d972e3 100644 --- a/postgres/drizzle-orm-test/src/proxy-client.ts +++ b/postgres/drizzle-orm-test/src/proxy-client.ts @@ -72,6 +72,7 @@ export function proxyClientQuery(db: PgTestClient): void { // For normal queries (Drizzle ORM), apply context before executing const ctxStmts = (db as any).ctxStmts as string | undefined; + if (!ctxStmts) { // No context to apply, just delegate return originalQuery(...args); @@ -81,6 +82,7 @@ export function proxyClientQuery(db: PgTestClient): void { // Return a Promise for Drizzle's async query pattern const runWithContext = async () => { await originalQuery(ctxStmts); + return originalQuery(...args); }; diff --git a/postgres/introspectron/__tests__/gql.test.ts b/postgres/introspectron/__tests__/gql.test.ts index 7a8787b9d..9ccaabf4a 100644 --- a/postgres/introspectron/__tests__/gql.test.ts +++ b/postgres/introspectron/__tests__/gql.test.ts @@ -9,6 +9,7 @@ let queriesDbe, mutationsDbe; beforeAll(() => { // const { queries, mutations } = parseGraphQuery(introQuery); const { queries, mutations } = parseGraphQuery(introQueryDbe); + queriesDbe = queries; mutationsDbe = mutations; }); @@ -24,6 +25,7 @@ it('mutationsDbe', () => { it('includes custom scalar types', () => { const actions = queriesDbe.actions; const names = actions.selection.map((sel) => (typeof sel === 'string' ? sel : sel.name)); + expect(names.includes('location')).toBeTruthy(); expect(names.includes('timeRequired')).toBeTruthy(); }); diff --git a/postgres/introspectron/__tests__/introspect.test.ts b/postgres/introspectron/__tests__/introspect.test.ts index 2b266dd1d..1cfd36762 100644 --- a/postgres/introspectron/__tests__/introspect.test.ts +++ b/postgres/introspectron/__tests__/introspect.test.ts @@ -7,6 +7,7 @@ const getDbString = (db) => `postgres://${process.env.PGUSER}:${process.env.PGPASSWORD}@${process.env.PGHOST}:${process.env.PGPORT}/${db}`; let pgPool; + beforeAll(() => { pgPool = new pg.Pool({ connectionString: getDbString('postgres') @@ -16,14 +17,16 @@ afterAll(() => { pgPool.end(); }); let raw; + xit('introspect', async () => { raw = await introspect(pgPool, { schemas: ['public'] }); expect(raw).toMatchSnapshot(); const processed = introspectionResultsFromRaw(raw); + require('fs').writeFileSync( - __dirname + '/introspect.json', + `${__dirname }/introspect.json`, jsonStringify(processed, null, 2) ); }); diff --git a/postgres/introspectron/__tests__/introspectron.gql.test.ts b/postgres/introspectron/__tests__/introspectron.gql.test.ts index e565c3af1..99f634c6a 100644 --- a/postgres/introspectron/__tests__/introspectron.gql.test.ts +++ b/postgres/introspectron/__tests__/introspectron.gql.test.ts @@ -37,12 +37,14 @@ beforeEach(async () => { it('includes basic root types', () => { const typeNames = introspection.__schema.types.map((t) => t.name); + expect(typeNames).toContain('Query'); expect(typeNames).toContain('String'); }); it('includes standard GraphQL directives', () => { const directiveNames = introspection.__schema.directives.map((d) => d.name); + expect(directiveNames).toContain('skip'); expect(directiveNames).toContain('include'); expect(directiveNames).toContain('deprecated'); @@ -50,16 +52,19 @@ it('includes standard GraphQL directives', () => { it('includes fields with arguments', () => { const testType = introspection.__schema.types.find((t) => t.name === 'Query'); + expect(testType?.fields?.length).toBeGreaterThan(0); const fieldWithArgs = testType?.fields?.find( (f: IntrospectionField) => f.args && f.args.length > 0 ); + expect(fieldWithArgs).toBeDefined(); }); it('has stable structure', () => { const schema = introspection.__schema; + expect(schema.queryType?.name).toBe('Query'); expect(Array.isArray(schema.types)).toBe(true); expect(schema.types.length).toBeGreaterThan(5); diff --git a/postgres/introspectron/__tests__/introspectron.sql.test.ts b/postgres/introspectron/__tests__/introspectron.sql.test.ts index 0a52090c6..714a6f0d0 100644 --- a/postgres/introspectron/__tests__/introspectron.sql.test.ts +++ b/postgres/introspectron/__tests__/introspectron.sql.test.ts @@ -43,16 +43,19 @@ describe('introspect() SQL-based introspection', () => { it('includes the introspectron schema', () => { const schemaNames = result.namespace.map((n) => n.name); + expect(schemaNames).toContain('introspectron'); }); it('freezes all introspected objects', () => { const anyClass = result.class[0]; + expect(Object.isFrozen(anyClass)).toBe(true); }); it('detects primary key constraints', () => { const primaryKeys = result.constraint.filter((c) => c.type === 'p'); + expect(primaryKeys.length).toBeGreaterThan(0); expect(primaryKeys[0]?.keyAttributeNums.length).toBeGreaterThan(0); }); @@ -76,6 +79,7 @@ describe('introspect() SQL-based introspection', () => { it('warns but does not throw if `pgThrowOnMissingSchema` is false', async () => { const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + await introspect(pg.client, { schemas: ['nonexistent_schema'], pgThrowOnMissingSchema: false diff --git a/postgres/introspectron/src/gql.ts b/postgres/introspectron/src/gql.ts index d5e571cda..f39643938 100644 --- a/postgres/introspectron/src/gql.ts +++ b/postgres/introspectron/src/gql.ts @@ -60,6 +60,7 @@ export const parseSingleQuery = ( model, properties: query.args.reduce((m2, v) => { m2[v.name] = getInputForQueries(v.type); + return m2; }, {} as Record), selection: parseSelectionScalar(context, model) @@ -71,6 +72,7 @@ export const parseSingleQuery = ( qtype: 'getOne', properties: query.args.reduce((m2, v) => { m2[v.name] = getInputForQueries(v.type); + return m2; }, {} as Record), selection: parseSelectionObject(context, model, 1) @@ -81,6 +83,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const types = introQuery.__schema.types; const HASH: Record = types.reduce((m, v) => { m[v.name] = v; + return m; }, {} as Record); @@ -90,6 +93,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const getInputForQueries = (input: IntrospectionTypeRef, context: ParseContext = {}): ParseContext => { if (input.kind === 'NON_NULL') { context.isNotNull = true; + return getInputForQueries(input.ofType!, context); } @@ -99,19 +103,24 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { context.isArrayNotNull = true; delete context.isNotNull; } + return getInputForQueries(input.ofType!, context); } if (input.kind === 'INPUT_OBJECT' && input.name && HASH.hasOwnProperty(input.name)) { const schema = HASH[input.name]; + context.properties = schema.inputFields!.map((field) => ({ name: field.name, type: field.type })).reduce((m3, v) => { m3[v.name] = v; + return m3; }, {} as Record); } else if (input.kind === 'OBJECT' && input.name && HASH.hasOwnProperty(input.name)) { const schema = HASH[input.name]; + context.properties = schema.fields!.map((field) => ({ name: field.name, type: field.type })).reduce((m3, v) => { m3[v.name] = v; + return m3; }, {} as Record); } else { @@ -129,6 +138,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const getInputForMutations = (input: IntrospectionTypeRef, context: ParseContext = {}): ParseContext => { if (input.kind === 'NON_NULL') { context.isNotNull = true; + return getInputForMutations(input.ofType!, context); } @@ -138,19 +148,24 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { context.isArrayNotNull = true; delete context.isNotNull; } + return getInputForMutations(input.ofType!, context); } if (input.kind === 'INPUT_OBJECT' && input.name && HASH.hasOwnProperty(input.name)) { const schema = HASH[input.name]; + context.properties = schema.inputFields!.map((field) => getInputForMutations(field.type, { name: field.name })).reduce((m3, v) => { m3[v.name!] = v; + return m3; }, {} as Record); } else if (input.kind === 'OBJECT' && input.name && HASH.hasOwnProperty(input.name)) { const schema = HASH[input.name]; + context.properties = schema.fields!.map((field) => ({ name: field.name, type: field.type })).reduce((m3, v) => { m3[v.name] = v; + return m3; }, {} as Record); } else { @@ -162,6 +177,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const mutations = (mutationsRoot?.fields || []).reduce((m, mutation) => { let mutationType = 'other'; + if (/^Create/.test(mutation.type.name!)) mutationType = 'create'; else if (/^Update/.test(mutation.type.name!)) mutationType = 'patch'; else if (/^Delete/.test(mutation.type.name!)) mutationType = 'delete'; @@ -169,15 +185,19 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const props = mutation.args.reduce((m2, arg) => { const type = arg.type?.ofType?.name; const isNotNull = arg.type?.kind === 'NON_NULL'; + if (type && HASH.hasOwnProperty(type)) { const schema = HASH[type]; const fields = schema.inputFields!.filter((a) => a.name !== 'clientMutationId'); const properties = fields.map((a) => getInputForMutations(a.type, { name: a.name })).reduce((m3, v) => { m3[v.name!] = v; + return m3; }, {} as Record); + m2[arg.name] = { isNotNull, type, properties }; } + return m2; }, {} as Record); @@ -185,13 +205,17 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { type.fields!.filter((t) => t.type.kind === 'OBJECT' && t.type.name !== 'Query').map((f) => ({ name: f.name, type: f.type })); const models = getModelTypes(HASH[mutation.type.name!]); + if (models.length > 0) { const model = models[0].type.name!; + m[mutation.name] = { qtype: 'mutation', mutationType, model, properties: props, output: mutation.type }; } else { let outputFields: any[] = []; + if (mutation.type.kind === 'OBJECT') { const t = HASH[mutation.type.name!]; + outputFields = t.fields!.map((f) => ({ name: f.name, type: f.type })).filter((f) => f.name !== 'clientMutationId' && f.type.name !== 'Query'); } m[mutation.name] = { qtype: 'mutation', mutationType, properties: props, output: mutation.type, outputs: outputFields }; @@ -202,6 +226,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { const queries = queriesRoot.fields!.reduce((m, query) => { const model = getObjectType(query.type); + if (model) { if (isConnectionQuery(query)) { m[query.name] = parseConnectionQuery(context, query, 1); @@ -209,6 +234,7 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { m[query.name] = parseSingleQuery(context, query, 1); } } + return m; }, {} as Record); @@ -220,18 +246,22 @@ export const parseGraphQuery = (introQuery: IntrospectionQueryResult) => { function parseSelectionObject(context: SelectionContext, model: string, nesting: number): any[] { const { HASH } = context; + throwIfInvalidContext(context); const selectionFields = HASH[model].fields! .filter((f) => !isPureObjectType(f.type)) .filter((f) => { const objectType = getObjectType(f.type); + if (objectType && fieldHasRequiredArgs(f)) return false; + return true; }); const mapped = selectionFields.map((f) => { const objectType = getObjectType(f.type); + if (objectType) { if (isConnectionQuery(f)) { return { name: f.name, ...parseConnectionQuery(context, f, nesting - 1) }; @@ -239,21 +269,27 @@ function parseSelectionObject(context: SelectionContext, model: string, nesting: if (isListType(f.type)) { return { name: f.name, selection: parseSelectionScalar(context, objectType) }; } + return { name: f.name, ...parseSingleQuery(context, f, nesting - 1) }; } const baseKind = getBaseKind(f.type); + if (isListType(f.type)) { if (baseKind === 'SCALAR' || baseKind === 'ENUM') return f.name; + return null as any; } if (baseKind === 'UNION' || baseKind === 'INTERFACE') return null as any; + return f.name; }); + return mapped.filter((x) => x != null); } function parseSelectionScalar(context: SelectionContext, model: string): string[] { const { HASH } = context; + throwIfInvalidContext(context); const selectionFields = HASH[model].fields!.filter( @@ -261,16 +297,21 @@ function parseSelectionScalar(context: SelectionContext, model: string): string[ if (isPureObjectType(f.type)) return false; if (isConnectionQuery(f)) return false; const objectType = getObjectType(f.type); + if (objectType) return false; const baseKind = getBaseKind(f.type); + if (isListType(f.type) && !(baseKind === 'SCALAR' || baseKind === 'ENUM')) return false; if (baseKind === 'UNION' || baseKind === 'INTERFACE') return false; + return true; } ); const names = selectionFields.map((f) => f.name); + if (names.length === 0) return ['__typename']; + return names; } @@ -292,12 +333,14 @@ function isPureObjectType(typeObj: IntrospectionTypeRef): boolean { function getObjectType(type: IntrospectionTypeRef): string | undefined { if (type.kind === 'OBJECT') return type.name || undefined; if (type.ofType) return getObjectType(type.ofType); + return undefined; } function isListType(type: IntrospectionTypeRef): boolean { if (type.kind === 'LIST') return true; if (type.ofType) return isListType(type.ofType); + return false; } @@ -307,17 +350,21 @@ function fieldHasRequiredArgs(field: IntrospectionField): boolean { return true; } } + return false; } function getBaseKind(type: IntrospectionTypeRef): string { let t: IntrospectionTypeRef | undefined | null = type; + while (t && t.ofType) t = t.ofType; + return (t?.kind || type.kind) as string; } function throwIfInvalidContext(context: SelectionContext): void { const { HASH, getInputForQueries } = context; + if (!HASH || !getInputForQueries) { throw new Error('parseSelection: context missing'); } diff --git a/postgres/introspectron/src/introspect.ts b/postgres/introspectron/src/introspect.ts index 19f0294a3..2a5a476f7 100644 --- a/postgres/introspectron/src/introspect.ts +++ b/postgres/introspectron/src/introspect.ts @@ -85,6 +85,7 @@ export const introspect = async ( object.comment = object.description; if (pgEnableTags && object.description) { const parsed = parseTags(object.description); + object.tags = parsed.tags; object.description = parsed.text; } else { @@ -96,6 +97,7 @@ export const introspect = async ( const extensionConfigurationClassIds = (result.extension as any[]).flatMap( (e) => e.configurationClassIds || [] ); + for (const klass of result.class as any[]) { klass.isExtensionConfigurationTable = extensionConfigurationClassIds.includes(klass.id); @@ -116,10 +118,11 @@ export const introspect = async ( )}'; however we couldn't find some of those! Missing schemas are: '${missingSchemas.join( "', '" )}'`; + if (pgThrowOnMissingSchema) { throw new Error(errorMessage); } else { - console.warn('⚠️ WARNING⚠️ ' + errorMessage); + console.warn(`⚠️ WARNING⚠️ ${ errorMessage}`); } } diff --git a/postgres/introspectron/src/introspectGql.ts b/postgres/introspectron/src/introspectGql.ts index 9a087bd3f..56e000daf 100644 --- a/postgres/introspectron/src/introspectGql.ts +++ b/postgres/introspectron/src/introspectGql.ts @@ -1,5 +1,5 @@ -import gql from 'graphql-tag'; import { DocumentNode } from 'graphql'; +import gql from 'graphql-tag'; export const IntrospectionQuery: DocumentNode = gql` query IntrospectionQuery { diff --git a/postgres/introspectron/src/process.ts b/postgres/introspectron/src/process.ts index 2328e735e..29aad8baf 100644 --- a/postgres/introspectron/src/process.ts +++ b/postgres/introspectron/src/process.ts @@ -9,31 +9,37 @@ import { deepClone, parseTags } from './utils'; const removeQuotes = (str: string): string => { const trimmed = str.trim(); + if (trimmed[0] === '"') { if (trimmed[trimmed.length - 1] !== '"') { throw new Error( `We failed to parse a quoted identifier '${str}'. Please avoid putting quotes or commas in smart comment identifiers (or file a PR to fix the parser).` ); } + return trimmed.substring(1, trimmed.length - 1); - } else { - return trimmed.toLowerCase(); } + + return trimmed.toLowerCase(); + }; const parseSqlColumnArray = (str: string): string[] => { if (!str) throw new Error(`Cannot parse '${str}'`); + return str.split(',').map(removeQuotes); }; const parseSqlColumnString = (str: string): string => { if (!str) throw new Error(`Cannot parse '${str}'`); + return removeQuotes(str); }; function parseConstraintSpec(rawSpec: string): { spec: string; tags: Record; description: string } { const [spec, ...tagComponents] = rawSpec.split(/\|/); const parsed = parseTags(tagComponents.join('\n')); + return { spec, tags: parsed.tags, @@ -50,27 +56,32 @@ function smartCommentConstraints(introspectionResults: PgIntrospectionResultByKi const attributes = introspectionResults.attribute .filter((a) => a.classId === tbl.id) .sort((a, b) => a.num - b.num); + if (!cols) { const pk = introspectionResults.constraint.find( (c) => c.classId === tbl.id && c.type === 'p' ); + if (pk) { return pk.keyAttributeNums.map((n) => attributes.find((a) => a.num === n)! ); - } else { + } throw new Error( `No columns specified for '${tbl.namespaceName}.${tbl.name}' (oid: ${tbl.id}) and no PK found (${debugStr}).` ); - } + } + return cols.map((colName) => { const attr = attributes.find((a) => a.name === colName); + if (!attr) { throw new Error( `Could not find attribute '${colName}' in '${tbl.namespaceName}.${tbl.name}'` ); } + return attr; }); }; @@ -87,6 +98,7 @@ export const introspectionResultsFromRaw = ( const xByY = >(arrayOfX: T[], attrKey: keyof T): Record => arrayOfX.reduce((memo, x) => { memo[x[attrKey]] = x; + return memo; }, {} as Record); @@ -94,8 +106,10 @@ export const introspectionResultsFromRaw = ( arrayOfX.reduce((memo, x) => { const k1 = x[key1]; const k2 = x[key2]; + if (!memo[k1]) memo[k1] = {}; memo[k1][k2] = x; + return memo; }, {} as Record>); @@ -114,19 +128,23 @@ export const introspectionResultsFromRaw = ( ) => { array.forEach((entry: any) => { const key = entry[lookupAttr]; + if (Array.isArray(key)) { entry[newAttr] = key .map((innerKey) => { const result = lookup[innerKey]; + if (innerKey && !result && !missingOk) { // @ts-ignore throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' ('${innerKey}') on '${JSON.stringify(entry)}'`); } + return result; }) .filter(Boolean); } else { const result = lookup[key]; + if (key && !result && !missingOk) { // @ts-ignore throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' on '${JSON.stringify(entry)}'`); @@ -141,6 +159,7 @@ export const introspectionResultsFromRaw = ( fn ? fn(introspectionResults) : null ); }; + augment(introspectionResultsByKind); relate(introspectionResultsByKind.class, 'namespace', 'namespaceId', introspectionResultsByKind.namespaceById, true); @@ -186,6 +205,7 @@ export const introspectionResultsFromRaw = ( introspectionResultsByKind.index.forEach((index: PgIndex) => { const columns = index.attributeNums.map((nr) => index.class.attributes.find((attr) => attr.num === nr)); + if (columns[0]) { columns[0].isIndexed = true; } diff --git a/postgres/introspectron/src/query.ts b/postgres/introspectron/src/query.ts index bde55b5cc..0115c76bf 100644 --- a/postgres/introspectron/src/query.ts +++ b/postgres/introspectron/src/query.ts @@ -16,6 +16,7 @@ export const makeIntrospectionQuery = (serverVersionNum, options = {}) => { where pg_auth_members.roleid = pg_roles.oid and pg_auth_members.member = accessible_roles._oid `; + return `\ -- @see https://www.postgresql.org/docs/9.5/static/catalogs.html -- @see https://github.com/graphile/graphile-engine/blob/master/packages/graphile-build-pg/src/plugins/introspectionQuery.js diff --git a/postgres/introspectron/src/utils.ts b/postgres/introspectron/src/utils.ts index 9ed47d9b0..9f94957e6 100644 --- a/postgres/introspectron/src/utils.ts +++ b/postgres/introspectron/src/utils.ts @@ -10,11 +10,13 @@ export const parseTags = ( return { ...prev, text: `${prev.text}\n${curr}` }; } const match = curr.match(/^@[a-zA-Z][a-zA-Z0-9_]*($|\s)/); + if (!match) { return { ...prev, text: curr }; } const key = match[0].substr(1).trim(); const value = match[0] === curr ? true : curr.replace(match[0], ''); + return { ...prev, tags: { @@ -37,12 +39,14 @@ export const parseTags = ( export const deepClone = (value: T): T => { if (Array.isArray(value)) { return value.map((val) => deepClone(val)) as unknown as T; - } else if (typeof value === 'object' && value !== null) { + } if (typeof value === 'object' && value !== null) { return Object.keys(value).reduce((memo, k) => { (memo as any)[k] = deepClone((value as any)[k]); + return memo; }, {} as any) as T; - } else { - return value; } + + return value; + }; diff --git a/postgres/pg-ast/__tests__/pg-ast.test.ts b/postgres/pg-ast/__tests__/pg-ast.test.ts index a1f63d01a..4fb7b9ff2 100644 --- a/postgres/pg-ast/__tests__/pg-ast.test.ts +++ b/postgres/pg-ast/__tests__/pg-ast.test.ts @@ -9,6 +9,7 @@ it('deparses', async () => { lexpr: t.nodes.integer({ ival: 0 }), rexpr: t.nodes.integer({ ival: 0 }) }); + expect(obj).toMatchSnapshot(); expect(await deparse([obj])).toMatchSnapshot(); }); diff --git a/postgres/pg-ast/src/asts.ts b/postgres/pg-ast/src/asts.ts index e816d9065..8e0e7c7fe 100644 --- a/postgres/pg-ast/src/asts.ts +++ b/postgres/pg-ast/src/asts.ts @@ -3,63 +3,84 @@ * DO NOT MODIFY IT BY HAND. Instead, modify the source proto file, * and run the pg-proto-parser generate command to regenerate this file. */ +import { A_ArrayExpr, A_Const, A_Expr, A_Indices, A_Indirection, A_Star, AccessPriv, Aggref, Alias, AlterCollationStmt, AlterDatabaseRefreshCollStmt, AlterDatabaseSetStmt, AlterDatabaseStmt, AlterDefaultPrivilegesStmt, AlterDomainStmt, AlterEnumStmt, AlterEventTrigStmt, AlterExtensionContentsStmt, AlterExtensionStmt, AlterFdwStmt, AlterForeignServerStmt, AlterFunctionStmt, AlternativeSubPlan, AlterObjectDependsStmt, AlterObjectSchemaStmt, AlterOperatorStmt, AlterOpFamilyStmt, AlterOwnerStmt, AlterPolicyStmt, AlterPublicationStmt, AlterRoleSetStmt, AlterRoleStmt, AlterSeqStmt, AlterStatsStmt, AlterSubscriptionStmt, AlterSystemStmt, AlterTableCmd, AlterTableMoveAllStmt, AlterTableSpaceOptionsStmt, AlterTableStmt, AlterTSConfigurationStmt, AlterTSDictionaryStmt, AlterTypeStmt, AlterUserMappingStmt, ArrayCoerceExpr, ArrayExpr, BitString, Boolean, BooleanTest, BoolExpr, CallContext, CallStmt, CaseExpr, CaseTestExpr, CaseWhen, CheckPointStmt, ClosePortalStmt, ClusterStmt, CoalesceExpr, CoerceToDomain, CoerceToDomainValue, CoerceViaIO, CollateClause, CollateExpr, ColumnDef, ColumnRef, CommentStmt, CommonTableExpr, CompositeTypeStmt, Constraint, ConstraintsSetStmt, ConvertRowtypeExpr, CopyStmt, CreateAmStmt, CreateCastStmt, CreateConversionStmt, CreatedbStmt, CreateDomainStmt, CreateEnumStmt, CreateEventTrigStmt, CreateExtensionStmt, CreateFdwStmt, CreateForeignServerStmt, CreateForeignTableStmt, CreateFunctionStmt, CreateOpClassItem, CreateOpClassStmt, CreateOpFamilyStmt, CreatePLangStmt, CreatePolicyStmt, CreatePublicationStmt, CreateRangeStmt, CreateRoleStmt, CreateSchemaStmt, CreateSeqStmt, CreateStatsStmt, CreateStmt, CreateSubscriptionStmt, CreateTableAsStmt, CreateTableSpaceStmt, CreateTransformStmt, CreateTrigStmt, CreateUserMappingStmt, CTECycleClause, CTESearchClause, CurrentOfExpr, DeallocateStmt, DeclareCursorStmt, DefElem, DefineStmt, DeleteStmt, DiscardStmt, DistinctExpr, DoStmt, DropdbStmt, DropOwnedStmt, DropRoleStmt, DropStmt, DropSubscriptionStmt, DropTableSpaceStmt, DropUserMappingStmt, ExecuteStmt, ExplainStmt, FetchStmt, FieldSelect, FieldStore, Float, FromExpr, FuncCall, FuncExpr, FunctionParameter, GrantRoleStmt, GrantStmt, GroupingFunc, GroupingSet, ImportForeignSchemaStmt, IndexElem, IndexStmt, InferClause, InferenceElem, InlineCodeBlock, InsertStmt, Integer, IntList, IntoClause, JoinExpr, JsonAggConstructor, JsonArgument, JsonArrayAgg, JsonArrayConstructor, JsonArrayQueryConstructor, JsonBehavior, JsonConstructorExpr, JsonExpr, JsonFormat, JsonFuncExpr, JsonIsPredicate, JsonKeyValue, JsonObjectAgg, JsonObjectConstructor, JsonOutput, JsonParseExpr, JsonReturning, JsonScalarExpr, JsonSerializeExpr, JsonTable, JsonTableColumn, JsonTablePath, JsonTablePathScan, JsonTablePathSpec, JsonTableSiblingJoin, JsonValueExpr, List, ListenStmt, LoadStmt, LockingClause, LockStmt, MergeAction, MergeStmt, MergeSupportFunc, MergeWhenClause, MinMaxExpr, MultiAssignRef, NamedArgExpr, NextValueExpr, NotifyStmt, NullIfExpr, NullTest, ObjectWithArgs, OidList, OnConflictClause, OnConflictExpr, OpExpr, Param, ParamRef, ParseResult, PartitionBoundSpec, PartitionCmd, PartitionElem, PartitionRangeDatum, PartitionSpec, PLAssignStmt, PrepareStmt, PublicationObjSpec, PublicationTable, Query, RangeFunction, RangeSubselect, RangeTableFunc, RangeTableFuncCol, RangeTableSample, RangeTblEntry, RangeTblFunction, RangeTblRef, RangeVar, RawStmt, ReassignOwnedStmt, RefreshMatViewStmt, ReindexStmt, RelabelType, RenameStmt, ReplicaIdentityStmt, ResTarget, ReturnStmt, RoleSpec, RowCompareExpr, RowExpr, RowMarkClause, RTEPermissionInfo, RuleStmt, ScalarArrayOpExpr, ScanResult, ScanToken,SecLabelStmt, SelectStmt, SetOperationStmt, SetToDefault, SinglePartitionSpec, SortBy, SortGroupClause, SQLValueFunction, StatsElem, String, SubLink, SubPlan, SubscriptingRef, TableFunc, TableLikeClause, TableSampleClause, TargetEntry, TransactionStmt, TriggerTransition, TruncateStmt, TypeCast, TypeName, UnlistenStmt, UpdateStmt, VacuumRelation, VacuumStmt, Var, VariableSetStmt, VariableShowStmt, ViewStmt, WindowClause, WindowDef, WindowFunc, WindowFuncRunCondition, WithCheckOption, WithClause, XmlExpr, XmlSerialize } from "@pgsql/types"; import _o from "nested-obj"; -import { ParseResult, ScanResult, Integer, Float, Boolean, String, BitString, List, OidList, IntList, A_Const, Alias, RangeVar, TableFunc, IntoClause, Var, Param, Aggref, GroupingFunc, WindowFunc, WindowFuncRunCondition, MergeSupportFunc, SubscriptingRef, FuncExpr, NamedArgExpr, OpExpr, DistinctExpr, NullIfExpr, ScalarArrayOpExpr, BoolExpr, SubLink, SubPlan, AlternativeSubPlan, FieldSelect, FieldStore, RelabelType, CoerceViaIO, ArrayCoerceExpr, ConvertRowtypeExpr, CollateExpr, CaseExpr, CaseWhen, CaseTestExpr, ArrayExpr, RowExpr, RowCompareExpr, CoalesceExpr, MinMaxExpr, SQLValueFunction, XmlExpr, JsonFormat, JsonReturning, JsonValueExpr, JsonConstructorExpr, JsonIsPredicate, JsonBehavior, JsonExpr, JsonTablePath, JsonTablePathScan, JsonTableSiblingJoin, NullTest, BooleanTest, MergeAction, CoerceToDomain, CoerceToDomainValue, SetToDefault, CurrentOfExpr, NextValueExpr, InferenceElem, TargetEntry, RangeTblRef, JoinExpr, FromExpr, OnConflictExpr, Query, TypeName, ColumnRef, ParamRef, A_Expr, TypeCast, CollateClause, RoleSpec, FuncCall, A_Star, A_Indices, A_Indirection, A_ArrayExpr, ResTarget, MultiAssignRef, SortBy, WindowDef, RangeSubselect, RangeFunction, RangeTableFunc, RangeTableFuncCol, RangeTableSample, ColumnDef, TableLikeClause, IndexElem, DefElem, LockingClause, XmlSerialize, PartitionElem, PartitionSpec, PartitionBoundSpec, PartitionRangeDatum, SinglePartitionSpec, PartitionCmd, RangeTblEntry, RTEPermissionInfo, RangeTblFunction, TableSampleClause, WithCheckOption, SortGroupClause, GroupingSet, WindowClause, RowMarkClause, WithClause, InferClause, OnConflictClause, CTESearchClause, CTECycleClause, CommonTableExpr, MergeWhenClause, TriggerTransition, JsonOutput, JsonArgument, JsonFuncExpr, JsonTablePathSpec, JsonTable, JsonTableColumn, JsonKeyValue, JsonParseExpr, JsonScalarExpr, JsonSerializeExpr, JsonObjectConstructor, JsonArrayConstructor, JsonArrayQueryConstructor, JsonAggConstructor, JsonObjectAgg, JsonArrayAgg, RawStmt, InsertStmt, DeleteStmt, UpdateStmt, MergeStmt, SelectStmt, SetOperationStmt, ReturnStmt, PLAssignStmt, CreateSchemaStmt, AlterTableStmt, ReplicaIdentityStmt, AlterTableCmd, AlterCollationStmt, AlterDomainStmt, GrantStmt, ObjectWithArgs, AccessPriv, GrantRoleStmt, AlterDefaultPrivilegesStmt, CopyStmt, VariableSetStmt, VariableShowStmt, CreateStmt, Constraint, CreateTableSpaceStmt, DropTableSpaceStmt, AlterTableSpaceOptionsStmt, AlterTableMoveAllStmt, CreateExtensionStmt, AlterExtensionStmt, AlterExtensionContentsStmt, CreateFdwStmt, AlterFdwStmt, CreateForeignServerStmt, AlterForeignServerStmt, CreateForeignTableStmt, CreateUserMappingStmt, AlterUserMappingStmt, DropUserMappingStmt, ImportForeignSchemaStmt, CreatePolicyStmt, AlterPolicyStmt, CreateAmStmt, CreateTrigStmt, CreateEventTrigStmt, AlterEventTrigStmt, CreatePLangStmt, CreateRoleStmt, AlterRoleStmt, AlterRoleSetStmt, DropRoleStmt, CreateSeqStmt, AlterSeqStmt, DefineStmt, CreateDomainStmt, CreateOpClassStmt, CreateOpClassItem, CreateOpFamilyStmt, AlterOpFamilyStmt, DropStmt, TruncateStmt, CommentStmt, SecLabelStmt, DeclareCursorStmt, ClosePortalStmt, FetchStmt, IndexStmt, CreateStatsStmt, StatsElem, AlterStatsStmt, CreateFunctionStmt, FunctionParameter, AlterFunctionStmt, DoStmt, InlineCodeBlock, CallStmt, CallContext, RenameStmt, AlterObjectDependsStmt, AlterObjectSchemaStmt, AlterOwnerStmt, AlterOperatorStmt, AlterTypeStmt, RuleStmt, NotifyStmt, ListenStmt, UnlistenStmt, TransactionStmt, CompositeTypeStmt, CreateEnumStmt, CreateRangeStmt, AlterEnumStmt, ViewStmt, LoadStmt, CreatedbStmt, AlterDatabaseStmt, AlterDatabaseRefreshCollStmt, AlterDatabaseSetStmt, DropdbStmt, AlterSystemStmt, ClusterStmt, VacuumStmt, VacuumRelation, ExplainStmt, CreateTableAsStmt, RefreshMatViewStmt, CheckPointStmt, DiscardStmt, LockStmt, ConstraintsSetStmt, ReindexStmt, CreateConversionStmt, CreateCastStmt, CreateTransformStmt, PrepareStmt, ExecuteStmt, DeallocateStmt, DropOwnedStmt, ReassignOwnedStmt, AlterTSDictionaryStmt, AlterTSConfigurationStmt, PublicationTable, PublicationObjSpec, CreatePublicationStmt, AlterPublicationStmt, CreateSubscriptionStmt, AlterSubscriptionStmt, DropSubscriptionStmt, ScanToken } from "@pgsql/types"; export default { parseResult(_p?: ParseResult): ParseResult { const _j = {} as ParseResult; + _o.set(_j, "version", _p?.version); _o.set(_j, "stmts", _p?.stmts); + return _j; }, scanResult(_p?: ScanResult): ScanResult { const _j = {} as ScanResult; + _o.set(_j, "version", _p?.version); _o.set(_j, "tokens", _p?.tokens); + return _j; }, integer(_p?: Integer): Integer { const _j = {} as Integer; + _o.set(_j, "ival", _p?.ival); + return _j; }, float(_p?: Float): Float { const _j = {} as Float; + _o.set(_j, "fval", _p?.fval); + return _j; }, boolean(_p?: Boolean): Boolean { const _j = {} as Boolean; + _o.set(_j, "boolval", _p?.boolval); + return _j; }, string(_p?: String): String { const _j = {} as String; + _o.set(_j, "sval", _p?.sval); + return _j; }, bitString(_p?: BitString): BitString { const _j = {} as BitString; + _o.set(_j, "bsval", _p?.bsval); + return _j; }, list(_p?: List): List { const _j = {} as List; + _o.set(_j, "items", _p?.items); + return _j; }, oidList(_p?: OidList): OidList { const _j = {} as OidList; + _o.set(_j, "items", _p?.items); + return _j; }, intList(_p?: IntList): IntList { const _j = {} as IntList; + _o.set(_j, "items", _p?.items); + return _j; }, aConst(_p?: A_Const): A_Const { const _j = {} as A_Const; + _o.set(_j, "ival", _p?.ival); _o.set(_j, "fval", _p?.fval); _o.set(_j, "boolval", _p?.boolval); @@ -67,16 +88,20 @@ export default { _o.set(_j, "bsval", _p?.bsval); _o.set(_j, "isnull", _p?.isnull); _o.set(_j, "location", _p?.location); + return _j; }, alias(_p?: Alias): Alias { const _j = {} as Alias; + _o.set(_j, "aliasname", _p?.aliasname); _o.set(_j, "colnames", _p?.colnames); + return _j; }, rangeVar(_p?: RangeVar): RangeVar { const _j = {} as RangeVar; + _o.set(_j, "catalogname", _p?.catalogname); _o.set(_j, "schemaname", _p?.schemaname); _o.set(_j, "relname", _p?.relname); @@ -84,10 +109,12 @@ export default { _o.set(_j, "relpersistence", _p?.relpersistence); _o.set(_j, "alias", _p?.alias); _o.set(_j, "location", _p?.location); + return _j; }, tableFunc(_p?: TableFunc): TableFunc { const _j = {} as TableFunc; + _o.set(_j, "functype", _p?.functype); _o.set(_j, "ns_uris", _p?.ns_uris); _o.set(_j, "ns_names", _p?.ns_names); @@ -105,10 +132,12 @@ export default { _o.set(_j, "plan", _p?.plan); _o.set(_j, "ordinalitycol", _p?.ordinalitycol); _o.set(_j, "location", _p?.location); + return _j; }, intoClause(_p?: IntoClause): IntoClause { const _j = {} as IntoClause; + _o.set(_j, "rel", _p?.rel); _o.set(_j, "colNames", _p?.colNames); _o.set(_j, "accessMethod", _p?.accessMethod); @@ -117,10 +146,12 @@ export default { _o.set(_j, "tableSpaceName", _p?.tableSpaceName); _o.set(_j, "viewQuery", _p?.viewQuery); _o.set(_j, "skipData", _p?.skipData); + return _j; }, var(_p?: Var): Var { const _j = {} as Var; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "varno", _p?.varno); _o.set(_j, "varattno", _p?.varattno); @@ -130,10 +161,12 @@ export default { _o.set(_j, "varnullingrels", _p?.varnullingrels); _o.set(_j, "varlevelsup", _p?.varlevelsup); _o.set(_j, "location", _p?.location); + return _j; }, param(_p?: Param): Param { const _j = {} as Param; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "paramkind", _p?.paramkind); _o.set(_j, "paramid", _p?.paramid); @@ -141,10 +174,12 @@ export default { _o.set(_j, "paramtypmod", _p?.paramtypmod); _o.set(_j, "paramcollid", _p?.paramcollid); _o.set(_j, "location", _p?.location); + return _j; }, aggref(_p?: Aggref): Aggref { const _j = {} as Aggref; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "aggfnoid", _p?.aggfnoid); _o.set(_j, "aggtype", _p?.aggtype); @@ -164,19 +199,23 @@ export default { _o.set(_j, "aggno", _p?.aggno); _o.set(_j, "aggtransno", _p?.aggtransno); _o.set(_j, "location", _p?.location); + return _j; }, groupingFunc(_p?: GroupingFunc): GroupingFunc { const _j = {} as GroupingFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "args", _p?.args); _o.set(_j, "refs", _p?.refs); _o.set(_j, "agglevelsup", _p?.agglevelsup); _o.set(_j, "location", _p?.location); + return _j; }, windowFunc(_p?: WindowFunc): WindowFunc { const _j = {} as WindowFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "winfnoid", _p?.winfnoid); _o.set(_j, "wintype", _p?.wintype); @@ -189,27 +228,33 @@ export default { _o.set(_j, "winstar", _p?.winstar); _o.set(_j, "winagg", _p?.winagg); _o.set(_j, "location", _p?.location); + return _j; }, windowFuncRunCondition(_p?: WindowFuncRunCondition): WindowFuncRunCondition { const _j = {} as WindowFuncRunCondition; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "wfunc_left", _p?.wfunc_left); _o.set(_j, "arg", _p?.arg); + return _j; }, mergeSupportFunc(_p?: MergeSupportFunc): MergeSupportFunc { const _j = {} as MergeSupportFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "msftype", _p?.msftype); _o.set(_j, "msfcollid", _p?.msfcollid); _o.set(_j, "location", _p?.location); + return _j; }, subscriptingRef(_p?: SubscriptingRef): SubscriptingRef { const _j = {} as SubscriptingRef; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "refcontainertype", _p?.refcontainertype); _o.set(_j, "refelemtype", _p?.refelemtype); @@ -220,10 +265,12 @@ export default { _o.set(_j, "reflowerindexpr", _p?.reflowerindexpr); _o.set(_j, "refexpr", _p?.refexpr); _o.set(_j, "refassgnexpr", _p?.refassgnexpr); + return _j; }, funcExpr(_p?: FuncExpr): FuncExpr { const _j = {} as FuncExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "funcid", _p?.funcid); _o.set(_j, "funcresulttype", _p?.funcresulttype); @@ -234,19 +281,23 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, namedArgExpr(_p?: NamedArgExpr): NamedArgExpr { const _j = {} as NamedArgExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "name", _p?.name); _o.set(_j, "argnumber", _p?.argnumber); _o.set(_j, "location", _p?.location); + return _j; }, opExpr(_p?: OpExpr): OpExpr { const _j = {} as OpExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -255,10 +306,12 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, distinctExpr(_p?: DistinctExpr): DistinctExpr { const _j = {} as DistinctExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -267,10 +320,12 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, nullIfExpr(_p?: NullIfExpr): NullIfExpr { const _j = {} as NullIfExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -279,28 +334,34 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, scalarArrayOpExpr(_p?: ScalarArrayOpExpr): ScalarArrayOpExpr { const _j = {} as ScalarArrayOpExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "useOr", _p?.useOr); _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, boolExpr(_p?: BoolExpr): BoolExpr { const _j = {} as BoolExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "boolop", _p?.boolop); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, subLink(_p?: SubLink): SubLink { const _j = {} as SubLink; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subLinkType", _p?.subLinkType); _o.set(_j, "subLinkId", _p?.subLinkId); @@ -308,10 +369,12 @@ export default { _o.set(_j, "operName", _p?.operName); _o.set(_j, "subselect", _p?.subselect); _o.set(_j, "location", _p?.location); + return _j; }, subPlan(_p?: SubPlan): SubPlan { const _j = {} as SubPlan; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subLinkType", _p?.subLinkType); _o.set(_j, "testexpr", _p?.testexpr); @@ -329,35 +392,43 @@ export default { _o.set(_j, "args", _p?.args); _o.set(_j, "startup_cost", _p?.startup_cost); _o.set(_j, "per_call_cost", _p?.per_call_cost); + return _j; }, alternativeSubPlan(_p?: AlternativeSubPlan): AlternativeSubPlan { const _j = {} as AlternativeSubPlan; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subplans", _p?.subplans); + return _j; }, fieldSelect(_p?: FieldSelect): FieldSelect { const _j = {} as FieldSelect; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "fieldnum", _p?.fieldnum); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "resulttypmod", _p?.resulttypmod); _o.set(_j, "resultcollid", _p?.resultcollid); + return _j; }, fieldStore(_p?: FieldStore): FieldStore { const _j = {} as FieldStore; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "newvals", _p?.newvals); _o.set(_j, "fieldnums", _p?.fieldnums); _o.set(_j, "resulttype", _p?.resulttype); + return _j; }, relabelType(_p?: RelabelType): RelabelType { const _j = {} as RelabelType; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); @@ -365,20 +436,24 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "relabelformat", _p?.relabelformat); _o.set(_j, "location", _p?.location); + return _j; }, coerceViaio(_p?: CoerceViaIO): CoerceViaIO { const _j = {} as CoerceViaIO; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coerceformat", _p?.coerceformat); _o.set(_j, "location", _p?.location); + return _j; }, arrayCoerceExpr(_p?: ArrayCoerceExpr): ArrayCoerceExpr { const _j = {} as ArrayCoerceExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "elemexpr", _p?.elemexpr); @@ -387,27 +462,33 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coerceformat", _p?.coerceformat); _o.set(_j, "location", _p?.location); + return _j; }, convertRowtypeExpr(_p?: ConvertRowtypeExpr): ConvertRowtypeExpr { const _j = {} as ConvertRowtypeExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "convertformat", _p?.convertformat); _o.set(_j, "location", _p?.location); + return _j; }, collateExpr(_p?: CollateExpr): CollateExpr { const _j = {} as CollateExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "collOid", _p?.collOid); _o.set(_j, "location", _p?.location); + return _j; }, caseExpr(_p?: CaseExpr): CaseExpr { const _j = {} as CaseExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "casetype", _p?.casetype); _o.set(_j, "casecollid", _p?.casecollid); @@ -415,26 +496,32 @@ export default { _o.set(_j, "args", _p?.args); _o.set(_j, "defresult", _p?.defresult); _o.set(_j, "location", _p?.location); + return _j; }, caseWhen(_p?: CaseWhen): CaseWhen { const _j = {} as CaseWhen; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "result", _p?.result); _o.set(_j, "location", _p?.location); + return _j; }, caseTestExpr(_p?: CaseTestExpr): CaseTestExpr { const _j = {} as CaseTestExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); + return _j; }, arrayExpr(_p?: ArrayExpr): ArrayExpr { const _j = {} as ArrayExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "array_typeid", _p?.array_typeid); _o.set(_j, "array_collid", _p?.array_collid); @@ -442,20 +529,24 @@ export default { _o.set(_j, "elements", _p?.elements); _o.set(_j, "multidims", _p?.multidims); _o.set(_j, "location", _p?.location); + return _j; }, rowExpr(_p?: RowExpr): RowExpr { const _j = {} as RowExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "args", _p?.args); _o.set(_j, "row_typeid", _p?.row_typeid); _o.set(_j, "row_format", _p?.row_format); _o.set(_j, "colnames", _p?.colnames); _o.set(_j, "location", _p?.location); + return _j; }, rowCompareExpr(_p?: RowCompareExpr): RowCompareExpr { const _j = {} as RowCompareExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "rctype", _p?.rctype); _o.set(_j, "opnos", _p?.opnos); @@ -463,19 +554,23 @@ export default { _o.set(_j, "inputcollids", _p?.inputcollids); _o.set(_j, "largs", _p?.largs); _o.set(_j, "rargs", _p?.rargs); + return _j; }, coalesceExpr(_p?: CoalesceExpr): CoalesceExpr { const _j = {} as CoalesceExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "coalescetype", _p?.coalescetype); _o.set(_j, "coalescecollid", _p?.coalescecollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, minMaxExpr(_p?: MinMaxExpr): MinMaxExpr { const _j = {} as MinMaxExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "minmaxtype", _p?.minmaxtype); _o.set(_j, "minmaxcollid", _p?.minmaxcollid); @@ -483,19 +578,23 @@ export default { _o.set(_j, "op", _p?.op); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return _j; }, sqlValueFunction(_p?: SQLValueFunction): SQLValueFunction { const _j = {} as SQLValueFunction; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "type", _p?.type); _o.set(_j, "typmod", _p?.typmod); _o.set(_j, "location", _p?.location); + return _j; }, xmlExpr(_p?: XmlExpr): XmlExpr { const _j = {} as XmlExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "name", _p?.name); @@ -507,31 +606,39 @@ export default { _o.set(_j, "type", _p?.type); _o.set(_j, "typmod", _p?.typmod); _o.set(_j, "location", _p?.location); + return _j; }, jsonFormat(_p?: JsonFormat): JsonFormat { const _j = {} as JsonFormat; + _o.set(_j, "format_type", _p?.format_type); _o.set(_j, "encoding", _p?.encoding); _o.set(_j, "location", _p?.location); + return _j; }, jsonReturning(_p?: JsonReturning): JsonReturning { const _j = {} as JsonReturning; + _o.set(_j, "format", _p?.format); _o.set(_j, "typid", _p?.typid); _o.set(_j, "typmod", _p?.typmod); + return _j; }, jsonValueExpr(_p?: JsonValueExpr): JsonValueExpr { const _j = {} as JsonValueExpr; + _o.set(_j, "raw_expr", _p?.raw_expr); _o.set(_j, "formatted_expr", _p?.formatted_expr); _o.set(_j, "format", _p?.format); + return _j; }, jsonConstructorExpr(_p?: JsonConstructorExpr): JsonConstructorExpr { const _j = {} as JsonConstructorExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "type", _p?.type); _o.set(_j, "args", _p?.args); @@ -541,27 +648,33 @@ export default { _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); _o.set(_j, "location", _p?.location); + return _j; }, jsonIsPredicate(_p?: JsonIsPredicate): JsonIsPredicate { const _j = {} as JsonIsPredicate; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "format", _p?.format); _o.set(_j, "item_type", _p?.item_type); _o.set(_j, "unique_keys", _p?.unique_keys); _o.set(_j, "location", _p?.location); + return _j; }, jsonBehavior(_p?: JsonBehavior): JsonBehavior { const _j = {} as JsonBehavior; + _o.set(_j, "btype", _p?.btype); _o.set(_j, "expr", _p?.expr); _o.set(_j, "coerce", _p?.coerce); _o.set(_j, "location", _p?.location); + return _j; }, jsonExpr(_p?: JsonExpr): JsonExpr { const _j = {} as JsonExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "column_name", _p?.column_name); @@ -579,59 +692,73 @@ export default { _o.set(_j, "omit_quotes", _p?.omit_quotes); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return _j; }, jsonTablePath(_p?: JsonTablePath): JsonTablePath { const _j = {} as JsonTablePath; + _o.set(_j, "name", _p?.name); + return _j; }, jsonTablePathScan(_p?: JsonTablePathScan): JsonTablePathScan { const _j = {} as JsonTablePathScan; + _o.set(_j, "plan", _p?.plan); _o.set(_j, "path", _p?.path); _o.set(_j, "errorOnError", _p?.errorOnError); _o.set(_j, "child", _p?.child); _o.set(_j, "colMin", _p?.colMin); _o.set(_j, "colMax", _p?.colMax); + return _j; }, jsonTableSiblingJoin(_p?: JsonTableSiblingJoin): JsonTableSiblingJoin { const _j = {} as JsonTableSiblingJoin; + _o.set(_j, "plan", _p?.plan); _o.set(_j, "lplan", _p?.lplan); _o.set(_j, "rplan", _p?.rplan); + return _j; }, nullTest(_p?: NullTest): NullTest { const _j = {} as NullTest; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "nulltesttype", _p?.nulltesttype); _o.set(_j, "argisrow", _p?.argisrow); _o.set(_j, "location", _p?.location); + return _j; }, booleanTest(_p?: BooleanTest): BooleanTest { const _j = {} as BooleanTest; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "booltesttype", _p?.booltesttype); _o.set(_j, "location", _p?.location); + return _j; }, mergeAction(_p?: MergeAction): MergeAction { const _j = {} as MergeAction; + _o.set(_j, "matchKind", _p?.matchKind); _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "override", _p?.override); _o.set(_j, "qual", _p?.qual); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "updateColnos", _p?.updateColnos); + return _j; }, coerceToDomain(_p?: CoerceToDomain): CoerceToDomain { const _j = {} as CoerceToDomain; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); @@ -639,51 +766,63 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coercionformat", _p?.coercionformat); _o.set(_j, "location", _p?.location); + return _j; }, coerceToDomainValue(_p?: CoerceToDomainValue): CoerceToDomainValue { const _j = {} as CoerceToDomainValue; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return _j; }, setToDefault(_p?: SetToDefault): SetToDefault { const _j = {} as SetToDefault; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return _j; }, currentOfExpr(_p?: CurrentOfExpr): CurrentOfExpr { const _j = {} as CurrentOfExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "cvarno", _p?.cvarno); _o.set(_j, "cursor_name", _p?.cursor_name); _o.set(_j, "cursor_param", _p?.cursor_param); + return _j; }, nextValueExpr(_p?: NextValueExpr): NextValueExpr { const _j = {} as NextValueExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "seqid", _p?.seqid); _o.set(_j, "typeId", _p?.typeId); + return _j; }, inferenceElem(_p?: InferenceElem): InferenceElem { const _j = {} as InferenceElem; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "infercollid", _p?.infercollid); _o.set(_j, "inferopclass", _p?.inferopclass); + return _j; }, targetEntry(_p?: TargetEntry): TargetEntry { const _j = {} as TargetEntry; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "resno", _p?.resno); @@ -692,15 +831,19 @@ export default { _o.set(_j, "resorigtbl", _p?.resorigtbl); _o.set(_j, "resorigcol", _p?.resorigcol); _o.set(_j, "resjunk", _p?.resjunk); + return _j; }, rangeTblRef(_p?: RangeTblRef): RangeTblRef { const _j = {} as RangeTblRef; + _o.set(_j, "rtindex", _p?.rtindex); + return _j; }, joinExpr(_p?: JoinExpr): JoinExpr { const _j = {} as JoinExpr; + _o.set(_j, "jointype", _p?.jointype); _o.set(_j, "isNatural", _p?.isNatural); _o.set(_j, "larg", _p?.larg); @@ -710,16 +853,20 @@ export default { _o.set(_j, "quals", _p?.quals); _o.set(_j, "alias", _p?.alias); _o.set(_j, "rtindex", _p?.rtindex); + return _j; }, fromExpr(_p?: FromExpr): FromExpr { const _j = {} as FromExpr; + _o.set(_j, "fromlist", _p?.fromlist); _o.set(_j, "quals", _p?.quals); + return _j; }, onConflictExpr(_p?: OnConflictExpr): OnConflictExpr { const _j = {} as OnConflictExpr; + _o.set(_j, "action", _p?.action); _o.set(_j, "arbiterElems", _p?.arbiterElems); _o.set(_j, "arbiterWhere", _p?.arbiterWhere); @@ -728,10 +875,12 @@ export default { _o.set(_j, "onConflictWhere", _p?.onConflictWhere); _o.set(_j, "exclRelIndex", _p?.exclRelIndex); _o.set(_j, "exclRelTlist", _p?.exclRelTlist); + return _j; }, query(_p?: Query): Query { const _j = {} as Query; + _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "querySource", _p?.querySource); _o.set(_j, "canSetTag", _p?.canSetTag); @@ -774,10 +923,12 @@ export default { _o.set(_j, "withCheckOptions", _p?.withCheckOptions); _o.set(_j, "stmt_location", _p?.stmt_location); _o.set(_j, "stmt_len", _p?.stmt_len); + return _j; }, typeName(_p?: TypeName): TypeName { const _j = {} as TypeName; + _o.set(_j, "names", _p?.names); _o.set(_j, "typeOid", _p?.typeOid); _o.set(_j, "setof", _p?.setof); @@ -786,52 +937,66 @@ export default { _o.set(_j, "typemod", _p?.typemod); _o.set(_j, "arrayBounds", _p?.arrayBounds); _o.set(_j, "location", _p?.location); + return _j; }, columnRef(_p?: ColumnRef): ColumnRef { const _j = {} as ColumnRef; + _o.set(_j, "fields", _p?.fields); _o.set(_j, "location", _p?.location); + return _j; }, paramRef(_p?: ParamRef): ParamRef { const _j = {} as ParamRef; + _o.set(_j, "number", _p?.number); _o.set(_j, "location", _p?.location); + return _j; }, aExpr(_p?: A_Expr): A_Expr { const _j = {} as A_Expr; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "name", _p?.name); _o.set(_j, "lexpr", _p?.lexpr); _o.set(_j, "rexpr", _p?.rexpr); _o.set(_j, "location", _p?.location); + return _j; }, typeCast(_p?: TypeCast): TypeCast { const _j = {} as TypeCast; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "location", _p?.location); + return _j; }, collateClause(_p?: CollateClause): CollateClause { const _j = {} as CollateClause; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "collname", _p?.collname); _o.set(_j, "location", _p?.location); + return _j; }, roleSpec(_p?: RoleSpec): RoleSpec { const _j = {} as RoleSpec; + _o.set(_j, "roletype", _p?.roletype); _o.set(_j, "rolename", _p?.rolename); _o.set(_j, "location", _p?.location); + return _j; }, funcCall(_p?: FuncCall): FuncCall { const _j = {} as FuncCall; + _o.set(_j, "funcname", _p?.funcname); _o.set(_j, "args", _p?.args); _o.set(_j, "agg_order", _p?.agg_order); @@ -843,57 +1008,72 @@ export default { _o.set(_j, "func_variadic", _p?.func_variadic); _o.set(_j, "funcformat", _p?.funcformat); _o.set(_j, "location", _p?.location); + return _j; }, aStar(_p?: A_Star): A_Star { const _j = {} as A_Star; + return _j; }, aIndices(_p?: A_Indices): A_Indices { const _j = {} as A_Indices; + _o.set(_j, "is_slice", _p?.is_slice); _o.set(_j, "lidx", _p?.lidx); _o.set(_j, "uidx", _p?.uidx); + return _j; }, aIndirection(_p?: A_Indirection): A_Indirection { const _j = {} as A_Indirection; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "indirection", _p?.indirection); + return _j; }, aArrayExpr(_p?: A_ArrayExpr): A_ArrayExpr { const _j = {} as A_ArrayExpr; + _o.set(_j, "elements", _p?.elements); _o.set(_j, "location", _p?.location); + return _j; }, resTarget(_p?: ResTarget): ResTarget { const _j = {} as ResTarget; + _o.set(_j, "name", _p?.name); _o.set(_j, "indirection", _p?.indirection); _o.set(_j, "val", _p?.val); _o.set(_j, "location", _p?.location); + return _j; }, multiAssignRef(_p?: MultiAssignRef): MultiAssignRef { const _j = {} as MultiAssignRef; + _o.set(_j, "source", _p?.source); _o.set(_j, "colno", _p?.colno); _o.set(_j, "ncolumns", _p?.ncolumns); + return _j; }, sortBy(_p?: SortBy): SortBy { const _j = {} as SortBy; + _o.set(_j, "node", _p?.node); _o.set(_j, "sortby_dir", _p?.sortby_dir); _o.set(_j, "sortby_nulls", _p?.sortby_nulls); _o.set(_j, "useOp", _p?.useOp); _o.set(_j, "location", _p?.location); + return _j; }, windowDef(_p?: WindowDef): WindowDef { const _j = {} as WindowDef; + _o.set(_j, "name", _p?.name); _o.set(_j, "refname", _p?.refname); _o.set(_j, "partitionClause", _p?.partitionClause); @@ -902,27 +1082,33 @@ export default { _o.set(_j, "startOffset", _p?.startOffset); _o.set(_j, "endOffset", _p?.endOffset); _o.set(_j, "location", _p?.location); + return _j; }, rangeSubselect(_p?: RangeSubselect): RangeSubselect { const _j = {} as RangeSubselect; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "subquery", _p?.subquery); _o.set(_j, "alias", _p?.alias); + return _j; }, rangeFunction(_p?: RangeFunction): RangeFunction { const _j = {} as RangeFunction; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "ordinality", _p?.ordinality); _o.set(_j, "is_rowsfrom", _p?.is_rowsfrom); _o.set(_j, "functions", _p?.functions); _o.set(_j, "alias", _p?.alias); _o.set(_j, "coldeflist", _p?.coldeflist); + return _j; }, rangeTableFunc(_p?: RangeTableFunc): RangeTableFunc { const _j = {} as RangeTableFunc; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "docexpr", _p?.docexpr); _o.set(_j, "rowexpr", _p?.rowexpr); @@ -930,10 +1116,12 @@ export default { _o.set(_j, "columns", _p?.columns); _o.set(_j, "alias", _p?.alias); _o.set(_j, "location", _p?.location); + return _j; }, rangeTableFuncCol(_p?: RangeTableFuncCol): RangeTableFuncCol { const _j = {} as RangeTableFuncCol; + _o.set(_j, "colname", _p?.colname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "for_ordinality", _p?.for_ordinality); @@ -941,19 +1129,23 @@ export default { _o.set(_j, "colexpr", _p?.colexpr); _o.set(_j, "coldefexpr", _p?.coldefexpr); _o.set(_j, "location", _p?.location); + return _j; }, rangeTableSample(_p?: RangeTableSample): RangeTableSample { const _j = {} as RangeTableSample; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "method", _p?.method); _o.set(_j, "args", _p?.args); _o.set(_j, "repeatable", _p?.repeatable); _o.set(_j, "location", _p?.location); + return _j; }, columnDef(_p?: ColumnDef): ColumnDef { const _j = {} as ColumnDef; + _o.set(_j, "colname", _p?.colname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "compression", _p?.compression); @@ -973,17 +1165,21 @@ export default { _o.set(_j, "constraints", _p?.constraints); _o.set(_j, "fdwoptions", _p?.fdwoptions); _o.set(_j, "location", _p?.location); + return _j; }, tableLikeClause(_p?: TableLikeClause): TableLikeClause { const _j = {} as TableLikeClause; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "options", _p?.options); _o.set(_j, "relationOid", _p?.relationOid); + return _j; }, indexElem(_p?: IndexElem): IndexElem { const _j = {} as IndexElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); _o.set(_j, "indexcolname", _p?.indexcolname); @@ -992,51 +1188,63 @@ export default { _o.set(_j, "opclassopts", _p?.opclassopts); _o.set(_j, "ordering", _p?.ordering); _o.set(_j, "nulls_ordering", _p?.nulls_ordering); + return _j; }, defElem(_p?: DefElem): DefElem { const _j = {} as DefElem; + _o.set(_j, "defnamespace", _p?.defnamespace); _o.set(_j, "defname", _p?.defname); _o.set(_j, "arg", _p?.arg); _o.set(_j, "defaction", _p?.defaction); _o.set(_j, "location", _p?.location); + return _j; }, lockingClause(_p?: LockingClause): LockingClause { const _j = {} as LockingClause; + _o.set(_j, "lockedRels", _p?.lockedRels); _o.set(_j, "strength", _p?.strength); _o.set(_j, "waitPolicy", _p?.waitPolicy); + return _j; }, xmlSerialize(_p?: XmlSerialize): XmlSerialize { const _j = {} as XmlSerialize; + _o.set(_j, "xmloption", _p?.xmloption); _o.set(_j, "expr", _p?.expr); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "indent", _p?.indent); _o.set(_j, "location", _p?.location); + return _j; }, partitionElem(_p?: PartitionElem): PartitionElem { const _j = {} as PartitionElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); _o.set(_j, "collation", _p?.collation); _o.set(_j, "opclass", _p?.opclass); _o.set(_j, "location", _p?.location); + return _j; }, partitionSpec(_p?: PartitionSpec): PartitionSpec { const _j = {} as PartitionSpec; + _o.set(_j, "strategy", _p?.strategy); _o.set(_j, "partParams", _p?.partParams); _o.set(_j, "location", _p?.location); + return _j; }, partitionBoundSpec(_p?: PartitionBoundSpec): PartitionBoundSpec { const _j = {} as PartitionBoundSpec; + _o.set(_j, "strategy", _p?.strategy); _o.set(_j, "is_default", _p?.is_default); _o.set(_j, "modulus", _p?.modulus); @@ -1045,28 +1253,35 @@ export default { _o.set(_j, "lowerdatums", _p?.lowerdatums); _o.set(_j, "upperdatums", _p?.upperdatums); _o.set(_j, "location", _p?.location); + return _j; }, partitionRangeDatum(_p?: PartitionRangeDatum): PartitionRangeDatum { const _j = {} as PartitionRangeDatum; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "value", _p?.value); _o.set(_j, "location", _p?.location); + return _j; }, singlePartitionSpec(_p?: SinglePartitionSpec): SinglePartitionSpec { const _j = {} as SinglePartitionSpec; + return _j; }, partitionCmd(_p?: PartitionCmd): PartitionCmd { const _j = {} as PartitionCmd; + _o.set(_j, "name", _p?.name); _o.set(_j, "bound", _p?.bound); _o.set(_j, "concurrent", _p?.concurrent); + return _j; }, rangeTblEntry(_p?: RangeTblEntry): RangeTblEntry { const _j = {} as RangeTblEntry; + _o.set(_j, "alias", _p?.alias); _o.set(_j, "eref", _p?.eref); _o.set(_j, "rtekind", _p?.rtekind); @@ -1099,10 +1314,12 @@ export default { _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "inFromCl", _p?.inFromCl); _o.set(_j, "securityQuals", _p?.securityQuals); + return _j; }, rtePermissionInfo(_p?: RTEPermissionInfo): RTEPermissionInfo { const _j = {} as RTEPermissionInfo; + _o.set(_j, "relid", _p?.relid); _o.set(_j, "inh", _p?.inh); _o.set(_j, "requiredPerms", _p?.requiredPerms); @@ -1110,10 +1327,12 @@ export default { _o.set(_j, "selectedCols", _p?.selectedCols); _o.set(_j, "insertedCols", _p?.insertedCols); _o.set(_j, "updatedCols", _p?.updatedCols); + return _j; }, rangeTblFunction(_p?: RangeTblFunction): RangeTblFunction { const _j = {} as RangeTblFunction; + _o.set(_j, "funcexpr", _p?.funcexpr); _o.set(_j, "funccolcount", _p?.funccolcount); _o.set(_j, "funccolnames", _p?.funccolnames); @@ -1121,42 +1340,52 @@ export default { _o.set(_j, "funccoltypmods", _p?.funccoltypmods); _o.set(_j, "funccolcollations", _p?.funccolcollations); _o.set(_j, "funcparams", _p?.funcparams); + return _j; }, tableSampleClause(_p?: TableSampleClause): TableSampleClause { const _j = {} as TableSampleClause; + _o.set(_j, "tsmhandler", _p?.tsmhandler); _o.set(_j, "args", _p?.args); _o.set(_j, "repeatable", _p?.repeatable); + return _j; }, withCheckOption(_p?: WithCheckOption): WithCheckOption { const _j = {} as WithCheckOption; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "relname", _p?.relname); _o.set(_j, "polname", _p?.polname); _o.set(_j, "qual", _p?.qual); _o.set(_j, "cascaded", _p?.cascaded); + return _j; }, sortGroupClause(_p?: SortGroupClause): SortGroupClause { const _j = {} as SortGroupClause; + _o.set(_j, "tleSortGroupRef", _p?.tleSortGroupRef); _o.set(_j, "eqop", _p?.eqop); _o.set(_j, "sortop", _p?.sortop); _o.set(_j, "nulls_first", _p?.nulls_first); _o.set(_j, "hashable", _p?.hashable); + return _j; }, groupingSet(_p?: GroupingSet): GroupingSet { const _j = {} as GroupingSet; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "content", _p?.content); _o.set(_j, "location", _p?.location); + return _j; }, windowClause(_p?: WindowClause): WindowClause { const _j = {} as WindowClause; + _o.set(_j, "name", _p?.name); _o.set(_j, "refname", _p?.refname); _o.set(_j, "partitionClause", _p?.partitionClause); @@ -1171,50 +1400,62 @@ export default { _o.set(_j, "inRangeNullsFirst", _p?.inRangeNullsFirst); _o.set(_j, "winref", _p?.winref); _o.set(_j, "copiedOrder", _p?.copiedOrder); + return _j; }, rowMarkClause(_p?: RowMarkClause): RowMarkClause { const _j = {} as RowMarkClause; + _o.set(_j, "rti", _p?.rti); _o.set(_j, "strength", _p?.strength); _o.set(_j, "waitPolicy", _p?.waitPolicy); _o.set(_j, "pushedDown", _p?.pushedDown); + return _j; }, withClause(_p?: WithClause): WithClause { const _j = {} as WithClause; + _o.set(_j, "ctes", _p?.ctes); _o.set(_j, "recursive", _p?.recursive); _o.set(_j, "location", _p?.location); + return _j; }, inferClause(_p?: InferClause): InferClause { const _j = {} as InferClause; + _o.set(_j, "indexElems", _p?.indexElems); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "conname", _p?.conname); _o.set(_j, "location", _p?.location); + return _j; }, onConflictClause(_p?: OnConflictClause): OnConflictClause { const _j = {} as OnConflictClause; + _o.set(_j, "action", _p?.action); _o.set(_j, "infer", _p?.infer); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "location", _p?.location); + return _j; }, cteSearchClause(_p?: CTESearchClause): CTESearchClause { const _j = {} as CTESearchClause; + _o.set(_j, "search_col_list", _p?.search_col_list); _o.set(_j, "search_breadth_first", _p?.search_breadth_first); _o.set(_j, "search_seq_column", _p?.search_seq_column); _o.set(_j, "location", _p?.location); + return _j; }, cteCycleClause(_p?: CTECycleClause): CTECycleClause { const _j = {} as CTECycleClause; + _o.set(_j, "cycle_col_list", _p?.cycle_col_list); _o.set(_j, "cycle_mark_column", _p?.cycle_mark_column); _o.set(_j, "cycle_mark_value", _p?.cycle_mark_value); @@ -1225,10 +1466,12 @@ export default { _o.set(_j, "cycle_mark_typmod", _p?.cycle_mark_typmod); _o.set(_j, "cycle_mark_collation", _p?.cycle_mark_collation); _o.set(_j, "cycle_mark_neop", _p?.cycle_mark_neop); + return _j; }, commonTableExpr(_p?: CommonTableExpr): CommonTableExpr { const _j = {} as CommonTableExpr; + _o.set(_j, "ctename", _p?.ctename); _o.set(_j, "aliascolnames", _p?.aliascolnames); _o.set(_j, "ctematerialized", _p?.ctematerialized); @@ -1242,39 +1485,49 @@ export default { _o.set(_j, "ctecoltypes", _p?.ctecoltypes); _o.set(_j, "ctecoltypmods", _p?.ctecoltypmods); _o.set(_j, "ctecolcollations", _p?.ctecolcollations); + return _j; }, mergeWhenClause(_p?: MergeWhenClause): MergeWhenClause { const _j = {} as MergeWhenClause; + _o.set(_j, "matchKind", _p?.matchKind); _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "override", _p?.override); _o.set(_j, "condition", _p?.condition); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "values", _p?.values); + return _j; }, triggerTransition(_p?: TriggerTransition): TriggerTransition { const _j = {} as TriggerTransition; + _o.set(_j, "name", _p?.name); _o.set(_j, "isNew", _p?.isNew); _o.set(_j, "isTable", _p?.isTable); + return _j; }, jsonOutput(_p?: JsonOutput): JsonOutput { const _j = {} as JsonOutput; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "returning", _p?.returning); + return _j; }, jsonArgument(_p?: JsonArgument): JsonArgument { const _j = {} as JsonArgument; + _o.set(_j, "val", _p?.val); _o.set(_j, "name", _p?.name); + return _j; }, jsonFuncExpr(_p?: JsonFuncExpr): JsonFuncExpr { const _j = {} as JsonFuncExpr; + _o.set(_j, "op", _p?.op); _o.set(_j, "column_name", _p?.column_name); _o.set(_j, "context_item", _p?.context_item); @@ -1286,18 +1539,22 @@ export default { _o.set(_j, "wrapper", _p?.wrapper); _o.set(_j, "quotes", _p?.quotes); _o.set(_j, "location", _p?.location); + return _j; }, jsonTablePathSpec(_p?: JsonTablePathSpec): JsonTablePathSpec { const _j = {} as JsonTablePathSpec; + _o.set(_j, "string", _p?.string); _o.set(_j, "name", _p?.name); _o.set(_j, "name_location", _p?.name_location); _o.set(_j, "location", _p?.location); + return _j; }, jsonTable(_p?: JsonTable): JsonTable { const _j = {} as JsonTable; + _o.set(_j, "context_item", _p?.context_item); _o.set(_j, "pathspec", _p?.pathspec); _o.set(_j, "passing", _p?.passing); @@ -1306,10 +1563,12 @@ export default { _o.set(_j, "alias", _p?.alias); _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "location", _p?.location); + return _j; }, jsonTableColumn(_p?: JsonTableColumn): JsonTableColumn { const _j = {} as JsonTableColumn; + _o.set(_j, "coltype", _p?.coltype); _o.set(_j, "name", _p?.name); _o.set(_j, "typeName", _p?.typeName); @@ -1321,95 +1580,119 @@ export default { _o.set(_j, "on_empty", _p?.on_empty); _o.set(_j, "on_error", _p?.on_error); _o.set(_j, "location", _p?.location); + return _j; }, jsonKeyValue(_p?: JsonKeyValue): JsonKeyValue { const _j = {} as JsonKeyValue; + _o.set(_j, "key", _p?.key); _o.set(_j, "value", _p?.value); + return _j; }, jsonParseExpr(_p?: JsonParseExpr): JsonParseExpr { const _j = {} as JsonParseExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "unique_keys", _p?.unique_keys); _o.set(_j, "location", _p?.location); + return _j; }, jsonScalarExpr(_p?: JsonScalarExpr): JsonScalarExpr { const _j = {} as JsonScalarExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "location", _p?.location); + return _j; }, jsonSerializeExpr(_p?: JsonSerializeExpr): JsonSerializeExpr { const _j = {} as JsonSerializeExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "location", _p?.location); + return _j; }, jsonObjectConstructor(_p?: JsonObjectConstructor): JsonObjectConstructor { const _j = {} as JsonObjectConstructor; + _o.set(_j, "exprs", _p?.exprs); _o.set(_j, "output", _p?.output); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); _o.set(_j, "location", _p?.location); + return _j; }, jsonArrayConstructor(_p?: JsonArrayConstructor): JsonArrayConstructor { const _j = {} as JsonArrayConstructor; + _o.set(_j, "exprs", _p?.exprs); _o.set(_j, "output", _p?.output); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "location", _p?.location); + return _j; }, jsonArrayQueryConstructor(_p?: JsonArrayQueryConstructor): JsonArrayQueryConstructor { const _j = {} as JsonArrayQueryConstructor; + _o.set(_j, "query", _p?.query); _o.set(_j, "output", _p?.output); _o.set(_j, "format", _p?.format); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "location", _p?.location); + return _j; }, jsonAggConstructor(_p?: JsonAggConstructor): JsonAggConstructor { const _j = {} as JsonAggConstructor; + _o.set(_j, "output", _p?.output); _o.set(_j, "agg_filter", _p?.agg_filter); _o.set(_j, "agg_order", _p?.agg_order); _o.set(_j, "over", _p?.over); _o.set(_j, "location", _p?.location); + return _j; }, jsonObjectAgg(_p?: JsonObjectAgg): JsonObjectAgg { const _j = {} as JsonObjectAgg; + _o.set(_j, "constructor", _p?.constructor); _o.set(_j, "arg", _p?.arg); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); + return _j; }, jsonArrayAgg(_p?: JsonArrayAgg): JsonArrayAgg { const _j = {} as JsonArrayAgg; + _o.set(_j, "constructor", _p?.constructor); _o.set(_j, "arg", _p?.arg); _o.set(_j, "absent_on_null", _p?.absent_on_null); + return _j; }, rawStmt(_p?: RawStmt): RawStmt { const _j = {} as RawStmt; + _o.set(_j, "stmt", _p?.stmt); _o.set(_j, "stmt_location", _p?.stmt_location); _o.set(_j, "stmt_len", _p?.stmt_len); + return _j; }, insertStmt(_p?: InsertStmt): InsertStmt { const _j = {} as InsertStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "cols", _p?.cols); _o.set(_j, "selectStmt", _p?.selectStmt); @@ -1417,39 +1700,47 @@ export default { _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); _o.set(_j, "override", _p?.override); + return _j; }, deleteStmt(_p?: DeleteStmt): DeleteStmt { const _j = {} as DeleteStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "usingClause", _p?.usingClause); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return _j; }, updateStmt(_p?: UpdateStmt): UpdateStmt { const _j = {} as UpdateStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "fromClause", _p?.fromClause); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return _j; }, mergeStmt(_p?: MergeStmt): MergeStmt { const _j = {} as MergeStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "sourceRelation", _p?.sourceRelation); _o.set(_j, "joinCondition", _p?.joinCondition); _o.set(_j, "mergeWhenClauses", _p?.mergeWhenClauses); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return _j; }, selectStmt(_p?: SelectStmt): SelectStmt { const _j = {} as SelectStmt; + _o.set(_j, "distinctClause", _p?.distinctClause); _o.set(_j, "intoClause", _p?.intoClause); _o.set(_j, "targetList", _p?.targetList); @@ -1470,10 +1761,12 @@ export default { _o.set(_j, "all", _p?.all); _o.set(_j, "larg", _p?.larg); _o.set(_j, "rarg", _p?.rarg); + return _j; }, setOperationStmt(_p?: SetOperationStmt): SetOperationStmt { const _j = {} as SetOperationStmt; + _o.set(_j, "op", _p?.op); _o.set(_j, "all", _p?.all); _o.set(_j, "larg", _p?.larg); @@ -1482,46 +1775,58 @@ export default { _o.set(_j, "colTypmods", _p?.colTypmods); _o.set(_j, "colCollations", _p?.colCollations); _o.set(_j, "groupClauses", _p?.groupClauses); + return _j; }, returnStmt(_p?: ReturnStmt): ReturnStmt { const _j = {} as ReturnStmt; + _o.set(_j, "returnval", _p?.returnval); + return _j; }, plAssignStmt(_p?: PLAssignStmt): PLAssignStmt { const _j = {} as PLAssignStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "indirection", _p?.indirection); _o.set(_j, "nnames", _p?.nnames); _o.set(_j, "val", _p?.val); _o.set(_j, "location", _p?.location); + return _j; }, createSchemaStmt(_p?: CreateSchemaStmt): CreateSchemaStmt { const _j = {} as CreateSchemaStmt; + _o.set(_j, "schemaname", _p?.schemaname); _o.set(_j, "authrole", _p?.authrole); _o.set(_j, "schemaElts", _p?.schemaElts); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return _j; }, alterTableStmt(_p?: AlterTableStmt): AlterTableStmt { const _j = {} as AlterTableStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "cmds", _p?.cmds); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, replicaIdentityStmt(_p?: ReplicaIdentityStmt): ReplicaIdentityStmt { const _j = {} as ReplicaIdentityStmt; + _o.set(_j, "identity_type", _p?.identity_type); _o.set(_j, "name", _p?.name); + return _j; }, alterTableCmd(_p?: AlterTableCmd): AlterTableCmd { const _j = {} as AlterTableCmd; + _o.set(_j, "subtype", _p?.subtype); _o.set(_j, "name", _p?.name); _o.set(_j, "num", _p?.num); @@ -1530,25 +1835,31 @@ export default { _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "recurse", _p?.recurse); + return _j; }, alterCollationStmt(_p?: AlterCollationStmt): AlterCollationStmt { const _j = {} as AlterCollationStmt; + _o.set(_j, "collname", _p?.collname); + return _j; }, alterDomainStmt(_p?: AlterDomainStmt): AlterDomainStmt { const _j = {} as AlterDomainStmt; + _o.set(_j, "subtype", _p?.subtype); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "name", _p?.name); _o.set(_j, "def", _p?.def); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, grantStmt(_p?: GrantStmt): GrantStmt { const _j = {} as GrantStmt; + _o.set(_j, "is_grant", _p?.is_grant); _o.set(_j, "targtype", _p?.targtype); _o.set(_j, "objtype", _p?.objtype); @@ -1558,40 +1869,50 @@ export default { _o.set(_j, "grant_option", _p?.grant_option); _o.set(_j, "grantor", _p?.grantor); _o.set(_j, "behavior", _p?.behavior); + return _j; }, objectWithArgs(_p?: ObjectWithArgs): ObjectWithArgs { const _j = {} as ObjectWithArgs; + _o.set(_j, "objname", _p?.objname); _o.set(_j, "objargs", _p?.objargs); _o.set(_j, "objfuncargs", _p?.objfuncargs); _o.set(_j, "args_unspecified", _p?.args_unspecified); + return _j; }, accessPriv(_p?: AccessPriv): AccessPriv { const _j = {} as AccessPriv; + _o.set(_j, "priv_name", _p?.priv_name); _o.set(_j, "cols", _p?.cols); + return _j; }, grantRoleStmt(_p?: GrantRoleStmt): GrantRoleStmt { const _j = {} as GrantRoleStmt; + _o.set(_j, "granted_roles", _p?.granted_roles); _o.set(_j, "grantee_roles", _p?.grantee_roles); _o.set(_j, "is_grant", _p?.is_grant); _o.set(_j, "opt", _p?.opt); _o.set(_j, "grantor", _p?.grantor); _o.set(_j, "behavior", _p?.behavior); + return _j; }, alterDefaultPrivilegesStmt(_p?: AlterDefaultPrivilegesStmt): AlterDefaultPrivilegesStmt { const _j = {} as AlterDefaultPrivilegesStmt; + _o.set(_j, "options", _p?.options); _o.set(_j, "action", _p?.action); + return _j; }, copyStmt(_p?: CopyStmt): CopyStmt { const _j = {} as CopyStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "query", _p?.query); _o.set(_j, "attlist", _p?.attlist); @@ -1600,23 +1921,29 @@ export default { _o.set(_j, "filename", _p?.filename); _o.set(_j, "options", _p?.options); _o.set(_j, "whereClause", _p?.whereClause); + return _j; }, variableSetStmt(_p?: VariableSetStmt): VariableSetStmt { const _j = {} as VariableSetStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "name", _p?.name); _o.set(_j, "args", _p?.args); _o.set(_j, "is_local", _p?.is_local); + return _j; }, variableShowStmt(_p?: VariableShowStmt): VariableShowStmt { const _j = {} as VariableShowStmt; + _o.set(_j, "name", _p?.name); + return _j; }, createStmt(_p?: CreateStmt): CreateStmt { const _j = {} as CreateStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "tableElts", _p?.tableElts); _o.set(_j, "inhRelations", _p?.inhRelations); @@ -1629,10 +1956,12 @@ export default { _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "accessMethod", _p?.accessMethod); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return _j; }, constraint(_p?: Constraint): Constraint { const _j = {} as Constraint; + _o.set(_j, "contype", _p?.contype); _o.set(_j, "conname", _p?.conname); _o.set(_j, "deferrable", _p?.deferrable); @@ -1664,132 +1993,166 @@ export default { _o.set(_j, "old_conpfeqop", _p?.old_conpfeqop); _o.set(_j, "old_pktable_oid", _p?.old_pktable_oid); _o.set(_j, "location", _p?.location); + return _j; }, createTableSpaceStmt(_p?: CreateTableSpaceStmt): CreateTableSpaceStmt { const _j = {} as CreateTableSpaceStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "owner", _p?.owner); _o.set(_j, "location", _p?.location); _o.set(_j, "options", _p?.options); + return _j; }, dropTableSpaceStmt(_p?: DropTableSpaceStmt): DropTableSpaceStmt { const _j = {} as DropTableSpaceStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, alterTableSpaceOptionsStmt(_p?: AlterTableSpaceOptionsStmt): AlterTableSpaceOptionsStmt { const _j = {} as AlterTableSpaceOptionsStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "options", _p?.options); _o.set(_j, "isReset", _p?.isReset); + return _j; }, alterTableMoveAllStmt(_p?: AlterTableMoveAllStmt): AlterTableMoveAllStmt { const _j = {} as AlterTableMoveAllStmt; + _o.set(_j, "orig_tablespacename", _p?.orig_tablespacename); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "roles", _p?.roles); _o.set(_j, "new_tablespacename", _p?.new_tablespacename); _o.set(_j, "nowait", _p?.nowait); + return _j; }, createExtensionStmt(_p?: CreateExtensionStmt): CreateExtensionStmt { const _j = {} as CreateExtensionStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return _j; }, alterExtensionStmt(_p?: AlterExtensionStmt): AlterExtensionStmt { const _j = {} as AlterExtensionStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "options", _p?.options); + return _j; }, alterExtensionContentsStmt(_p?: AlterExtensionContentsStmt): AlterExtensionContentsStmt { const _j = {} as AlterExtensionContentsStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "action", _p?.action); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); + return _j; }, createFdwStmt(_p?: CreateFdwStmt): CreateFdwStmt { const _j = {} as CreateFdwStmt; + _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "func_options", _p?.func_options); _o.set(_j, "options", _p?.options); + return _j; }, alterFdwStmt(_p?: AlterFdwStmt): AlterFdwStmt { const _j = {} as AlterFdwStmt; + _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "func_options", _p?.func_options); _o.set(_j, "options", _p?.options); + return _j; }, createForeignServerStmt(_p?: CreateForeignServerStmt): CreateForeignServerStmt { const _j = {} as CreateForeignServerStmt; + _o.set(_j, "servername", _p?.servername); _o.set(_j, "servertype", _p?.servertype); _o.set(_j, "version", _p?.version); _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return _j; }, alterForeignServerStmt(_p?: AlterForeignServerStmt): AlterForeignServerStmt { const _j = {} as AlterForeignServerStmt; + _o.set(_j, "servername", _p?.servername); _o.set(_j, "version", _p?.version); _o.set(_j, "options", _p?.options); _o.set(_j, "has_version", _p?.has_version); + return _j; }, createForeignTableStmt(_p?: CreateForeignTableStmt): CreateForeignTableStmt { const _j = {} as CreateForeignTableStmt; + _o.set(_j, "base", _p?.base); _o.set(_j, "servername", _p?.servername); _o.set(_j, "options", _p?.options); + return _j; }, createUserMappingStmt(_p?: CreateUserMappingStmt): CreateUserMappingStmt { const _j = {} as CreateUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return _j; }, alterUserMappingStmt(_p?: AlterUserMappingStmt): AlterUserMappingStmt { const _j = {} as AlterUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "options", _p?.options); + return _j; }, dropUserMappingStmt(_p?: DropUserMappingStmt): DropUserMappingStmt { const _j = {} as DropUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, importForeignSchemaStmt(_p?: ImportForeignSchemaStmt): ImportForeignSchemaStmt { const _j = {} as ImportForeignSchemaStmt; + _o.set(_j, "server_name", _p?.server_name); _o.set(_j, "remote_schema", _p?.remote_schema); _o.set(_j, "local_schema", _p?.local_schema); _o.set(_j, "list_type", _p?.list_type); _o.set(_j, "table_list", _p?.table_list); _o.set(_j, "options", _p?.options); + return _j; }, createPolicyStmt(_p?: CreatePolicyStmt): CreatePolicyStmt { const _j = {} as CreatePolicyStmt; + _o.set(_j, "policy_name", _p?.policy_name); _o.set(_j, "table", _p?.table); _o.set(_j, "cmd_name", _p?.cmd_name); @@ -1797,26 +2160,32 @@ export default { _o.set(_j, "roles", _p?.roles); _o.set(_j, "qual", _p?.qual); _o.set(_j, "with_check", _p?.with_check); + return _j; }, alterPolicyStmt(_p?: AlterPolicyStmt): AlterPolicyStmt { const _j = {} as AlterPolicyStmt; + _o.set(_j, "policy_name", _p?.policy_name); _o.set(_j, "table", _p?.table); _o.set(_j, "roles", _p?.roles); _o.set(_j, "qual", _p?.qual); _o.set(_j, "with_check", _p?.with_check); + return _j; }, createAmStmt(_p?: CreateAmStmt): CreateAmStmt { const _j = {} as CreateAmStmt; + _o.set(_j, "amname", _p?.amname); _o.set(_j, "handler_name", _p?.handler_name); _o.set(_j, "amtype", _p?.amtype); + return _j; }, createTrigStmt(_p?: CreateTrigStmt): CreateTrigStmt { const _j = {} as CreateTrigStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "isconstraint", _p?.isconstraint); _o.set(_j, "trigname", _p?.trigname); @@ -1832,78 +2201,98 @@ export default { _o.set(_j, "deferrable", _p?.deferrable); _o.set(_j, "initdeferred", _p?.initdeferred); _o.set(_j, "constrrel", _p?.constrrel); + return _j; }, createEventTrigStmt(_p?: CreateEventTrigStmt): CreateEventTrigStmt { const _j = {} as CreateEventTrigStmt; + _o.set(_j, "trigname", _p?.trigname); _o.set(_j, "eventname", _p?.eventname); _o.set(_j, "whenclause", _p?.whenclause); _o.set(_j, "funcname", _p?.funcname); + return _j; }, alterEventTrigStmt(_p?: AlterEventTrigStmt): AlterEventTrigStmt { const _j = {} as AlterEventTrigStmt; + _o.set(_j, "trigname", _p?.trigname); _o.set(_j, "tgenabled", _p?.tgenabled); + return _j; }, createpLangStmt(_p?: CreatePLangStmt): CreatePLangStmt { const _j = {} as CreatePLangStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "plname", _p?.plname); _o.set(_j, "plhandler", _p?.plhandler); _o.set(_j, "plinline", _p?.plinline); _o.set(_j, "plvalidator", _p?.plvalidator); _o.set(_j, "pltrusted", _p?.pltrusted); + return _j; }, createRoleStmt(_p?: CreateRoleStmt): CreateRoleStmt { const _j = {} as CreateRoleStmt; + _o.set(_j, "stmt_type", _p?.stmt_type); _o.set(_j, "role", _p?.role); _o.set(_j, "options", _p?.options); + return _j; }, alterRoleStmt(_p?: AlterRoleStmt): AlterRoleStmt { const _j = {} as AlterRoleStmt; + _o.set(_j, "role", _p?.role); _o.set(_j, "options", _p?.options); _o.set(_j, "action", _p?.action); + return _j; }, alterRoleSetStmt(_p?: AlterRoleSetStmt): AlterRoleSetStmt { const _j = {} as AlterRoleSetStmt; + _o.set(_j, "role", _p?.role); _o.set(_j, "database", _p?.database); _o.set(_j, "setstmt", _p?.setstmt); + return _j; }, dropRoleStmt(_p?: DropRoleStmt): DropRoleStmt { const _j = {} as DropRoleStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, createSeqStmt(_p?: CreateSeqStmt): CreateSeqStmt { const _j = {} as CreateSeqStmt; + _o.set(_j, "sequence", _p?.sequence); _o.set(_j, "options", _p?.options); _o.set(_j, "ownerId", _p?.ownerId); _o.set(_j, "for_identity", _p?.for_identity); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return _j; }, alterSeqStmt(_p?: AlterSeqStmt): AlterSeqStmt { const _j = {} as AlterSeqStmt; + _o.set(_j, "sequence", _p?.sequence); _o.set(_j, "options", _p?.options); _o.set(_j, "for_identity", _p?.for_identity); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, defineStmt(_p?: DefineStmt): DefineStmt { const _j = {} as DefineStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "oldstyle", _p?.oldstyle); _o.set(_j, "defnames", _p?.defnames); @@ -1911,103 +2300,129 @@ export default { _o.set(_j, "definition", _p?.definition); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "replace", _p?.replace); + return _j; }, createDomainStmt(_p?: CreateDomainStmt): CreateDomainStmt { const _j = {} as CreateDomainStmt; + _o.set(_j, "domainname", _p?.domainname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "collClause", _p?.collClause); _o.set(_j, "constraints", _p?.constraints); + return _j; }, createOpClassStmt(_p?: CreateOpClassStmt): CreateOpClassStmt { const _j = {} as CreateOpClassStmt; + _o.set(_j, "opclassname", _p?.opclassname); _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); _o.set(_j, "datatype", _p?.datatype); _o.set(_j, "items", _p?.items); _o.set(_j, "isDefault", _p?.isDefault); + return _j; }, createOpClassItem(_p?: CreateOpClassItem): CreateOpClassItem { const _j = {} as CreateOpClassItem; + _o.set(_j, "itemtype", _p?.itemtype); _o.set(_j, "name", _p?.name); _o.set(_j, "number", _p?.number); _o.set(_j, "order_family", _p?.order_family); _o.set(_j, "class_args", _p?.class_args); _o.set(_j, "storedtype", _p?.storedtype); + return _j; }, createOpFamilyStmt(_p?: CreateOpFamilyStmt): CreateOpFamilyStmt { const _j = {} as CreateOpFamilyStmt; + _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); + return _j; }, alterOpFamilyStmt(_p?: AlterOpFamilyStmt): AlterOpFamilyStmt { const _j = {} as AlterOpFamilyStmt; + _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); _o.set(_j, "isDrop", _p?.isDrop); _o.set(_j, "items", _p?.items); + return _j; }, dropStmt(_p?: DropStmt): DropStmt { const _j = {} as DropStmt; + _o.set(_j, "objects", _p?.objects); _o.set(_j, "removeType", _p?.removeType); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "concurrent", _p?.concurrent); + return _j; }, truncateStmt(_p?: TruncateStmt): TruncateStmt { const _j = {} as TruncateStmt; + _o.set(_j, "relations", _p?.relations); _o.set(_j, "restart_seqs", _p?.restart_seqs); _o.set(_j, "behavior", _p?.behavior); + return _j; }, commentStmt(_p?: CommentStmt): CommentStmt { const _j = {} as CommentStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); _o.set(_j, "comment", _p?.comment); + return _j; }, secLabelStmt(_p?: SecLabelStmt): SecLabelStmt { const _j = {} as SecLabelStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); _o.set(_j, "provider", _p?.provider); _o.set(_j, "label", _p?.label); + return _j; }, declareCursorStmt(_p?: DeclareCursorStmt): DeclareCursorStmt { const _j = {} as DeclareCursorStmt; + _o.set(_j, "portalname", _p?.portalname); _o.set(_j, "options", _p?.options); _o.set(_j, "query", _p?.query); + return _j; }, closePortalStmt(_p?: ClosePortalStmt): ClosePortalStmt { const _j = {} as ClosePortalStmt; + _o.set(_j, "portalname", _p?.portalname); + return _j; }, fetchStmt(_p?: FetchStmt): FetchStmt { const _j = {} as FetchStmt; + _o.set(_j, "direction", _p?.direction); _o.set(_j, "howMany", _p?.howMany); _o.set(_j, "portalname", _p?.portalname); _o.set(_j, "ismove", _p?.ismove); + return _j; }, indexStmt(_p?: IndexStmt): IndexStmt { const _j = {} as IndexStmt; + _o.set(_j, "idxname", _p?.idxname); _o.set(_j, "relation", _p?.relation); _o.set(_j, "accessMethod", _p?.accessMethod); @@ -2032,10 +2447,12 @@ export default { _o.set(_j, "concurrent", _p?.concurrent); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "reset_default_tblspc", _p?.reset_default_tblspc); + return _j; }, createStatsStmt(_p?: CreateStatsStmt): CreateStatsStmt { const _j = {} as CreateStatsStmt; + _o.set(_j, "defnames", _p?.defnames); _o.set(_j, "stat_types", _p?.stat_types); _o.set(_j, "exprs", _p?.exprs); @@ -2043,23 +2460,29 @@ export default { _o.set(_j, "stxcomment", _p?.stxcomment); _o.set(_j, "transformed", _p?.transformed); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return _j; }, statsElem(_p?: StatsElem): StatsElem { const _j = {} as StatsElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); + return _j; }, alterStatsStmt(_p?: AlterStatsStmt): AlterStatsStmt { const _j = {} as AlterStatsStmt; + _o.set(_j, "defnames", _p?.defnames); _o.set(_j, "stxstattarget", _p?.stxstattarget); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, createFunctionStmt(_p?: CreateFunctionStmt): CreateFunctionStmt { const _j = {} as CreateFunctionStmt; + _o.set(_j, "is_procedure", _p?.is_procedure); _o.set(_j, "replace", _p?.replace); _o.set(_j, "funcname", _p?.funcname); @@ -2067,50 +2490,64 @@ export default { _o.set(_j, "returnType", _p?.returnType); _o.set(_j, "options", _p?.options); _o.set(_j, "sql_body", _p?.sql_body); + return _j; }, functionParameter(_p?: FunctionParameter): FunctionParameter { const _j = {} as FunctionParameter; + _o.set(_j, "name", _p?.name); _o.set(_j, "argType", _p?.argType); _o.set(_j, "mode", _p?.mode); _o.set(_j, "defexpr", _p?.defexpr); + return _j; }, alterFunctionStmt(_p?: AlterFunctionStmt): AlterFunctionStmt { const _j = {} as AlterFunctionStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "func", _p?.func); _o.set(_j, "actions", _p?.actions); + return _j; }, doStmt(_p?: DoStmt): DoStmt { const _j = {} as DoStmt; + _o.set(_j, "args", _p?.args); + return _j; }, inlineCodeBlock(_p?: InlineCodeBlock): InlineCodeBlock { const _j = {} as InlineCodeBlock; + _o.set(_j, "source_text", _p?.source_text); _o.set(_j, "langOid", _p?.langOid); _o.set(_j, "langIsTrusted", _p?.langIsTrusted); _o.set(_j, "atomic", _p?.atomic); + return _j; }, callStmt(_p?: CallStmt): CallStmt { const _j = {} as CallStmt; + _o.set(_j, "funccall", _p?.funccall); _o.set(_j, "funcexpr", _p?.funcexpr); _o.set(_j, "outargs", _p?.outargs); + return _j; }, callContext(_p?: CallContext): CallContext { const _j = {} as CallContext; + _o.set(_j, "atomic", _p?.atomic); + return _j; }, renameStmt(_p?: RenameStmt): RenameStmt { const _j = {} as RenameStmt; + _o.set(_j, "renameType", _p?.renameType); _o.set(_j, "relationType", _p?.relationType); _o.set(_j, "relation", _p?.relation); @@ -2119,48 +2556,60 @@ export default { _o.set(_j, "newname", _p?.newname); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, alterObjectDependsStmt(_p?: AlterObjectDependsStmt): AlterObjectDependsStmt { const _j = {} as AlterObjectDependsStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "extname", _p?.extname); _o.set(_j, "remove", _p?.remove); + return _j; }, alterObjectSchemaStmt(_p?: AlterObjectSchemaStmt): AlterObjectSchemaStmt { const _j = {} as AlterObjectSchemaStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "newschema", _p?.newschema); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, alterOwnerStmt(_p?: AlterOwnerStmt): AlterOwnerStmt { const _j = {} as AlterOwnerStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "newowner", _p?.newowner); + return _j; }, alterOperatorStmt(_p?: AlterOperatorStmt): AlterOperatorStmt { const _j = {} as AlterOperatorStmt; + _o.set(_j, "opername", _p?.opername); _o.set(_j, "options", _p?.options); + return _j; }, alterTypeStmt(_p?: AlterTypeStmt): AlterTypeStmt { const _j = {} as AlterTypeStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "options", _p?.options); + return _j; }, ruleStmt(_p?: RuleStmt): RuleStmt { const _j = {} as RuleStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "rulename", _p?.rulename); _o.set(_j, "whereClause", _p?.whereClause); @@ -2168,252 +2617,325 @@ export default { _o.set(_j, "instead", _p?.instead); _o.set(_j, "actions", _p?.actions); _o.set(_j, "replace", _p?.replace); + return _j; }, notifyStmt(_p?: NotifyStmt): NotifyStmt { const _j = {} as NotifyStmt; + _o.set(_j, "conditionname", _p?.conditionname); _o.set(_j, "payload", _p?.payload); + return _j; }, listenStmt(_p?: ListenStmt): ListenStmt { const _j = {} as ListenStmt; + _o.set(_j, "conditionname", _p?.conditionname); + return _j; }, unlistenStmt(_p?: UnlistenStmt): UnlistenStmt { const _j = {} as UnlistenStmt; + _o.set(_j, "conditionname", _p?.conditionname); + return _j; }, transactionStmt(_p?: TransactionStmt): TransactionStmt { const _j = {} as TransactionStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "options", _p?.options); _o.set(_j, "savepoint_name", _p?.savepoint_name); _o.set(_j, "gid", _p?.gid); _o.set(_j, "chain", _p?.chain); _o.set(_j, "location", _p?.location); + return _j; }, compositeTypeStmt(_p?: CompositeTypeStmt): CompositeTypeStmt { const _j = {} as CompositeTypeStmt; + _o.set(_j, "typevar", _p?.typevar); _o.set(_j, "coldeflist", _p?.coldeflist); + return _j; }, createEnumStmt(_p?: CreateEnumStmt): CreateEnumStmt { const _j = {} as CreateEnumStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "vals", _p?.vals); + return _j; }, createRangeStmt(_p?: CreateRangeStmt): CreateRangeStmt { const _j = {} as CreateRangeStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "params", _p?.params); + return _j; }, alterEnumStmt(_p?: AlterEnumStmt): AlterEnumStmt { const _j = {} as AlterEnumStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "oldVal", _p?.oldVal); _o.set(_j, "newVal", _p?.newVal); _o.set(_j, "newValNeighbor", _p?.newValNeighbor); _o.set(_j, "newValIsAfter", _p?.newValIsAfter); _o.set(_j, "skipIfNewValExists", _p?.skipIfNewValExists); + return _j; }, viewStmt(_p?: ViewStmt): ViewStmt { const _j = {} as ViewStmt; + _o.set(_j, "view", _p?.view); _o.set(_j, "aliases", _p?.aliases); _o.set(_j, "query", _p?.query); _o.set(_j, "replace", _p?.replace); _o.set(_j, "options", _p?.options); _o.set(_j, "withCheckOption", _p?.withCheckOption); + return _j; }, loadStmt(_p?: LoadStmt): LoadStmt { const _j = {} as LoadStmt; + _o.set(_j, "filename", _p?.filename); + return _j; }, createdbStmt(_p?: CreatedbStmt): CreatedbStmt { const _j = {} as CreatedbStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "options", _p?.options); + return _j; }, alterDatabaseStmt(_p?: AlterDatabaseStmt): AlterDatabaseStmt { const _j = {} as AlterDatabaseStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "options", _p?.options); + return _j; }, alterDatabaseRefreshCollStmt(_p?: AlterDatabaseRefreshCollStmt): AlterDatabaseRefreshCollStmt { const _j = {} as AlterDatabaseRefreshCollStmt; + _o.set(_j, "dbname", _p?.dbname); + return _j; }, alterDatabaseSetStmt(_p?: AlterDatabaseSetStmt): AlterDatabaseSetStmt { const _j = {} as AlterDatabaseSetStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "setstmt", _p?.setstmt); + return _j; }, dropdbStmt(_p?: DropdbStmt): DropdbStmt { const _j = {} as DropdbStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "options", _p?.options); + return _j; }, alterSystemStmt(_p?: AlterSystemStmt): AlterSystemStmt { const _j = {} as AlterSystemStmt; + _o.set(_j, "setstmt", _p?.setstmt); + return _j; }, clusterStmt(_p?: ClusterStmt): ClusterStmt { const _j = {} as ClusterStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "indexname", _p?.indexname); _o.set(_j, "params", _p?.params); + return _j; }, vacuumStmt(_p?: VacuumStmt): VacuumStmt { const _j = {} as VacuumStmt; + _o.set(_j, "options", _p?.options); _o.set(_j, "rels", _p?.rels); _o.set(_j, "is_vacuumcmd", _p?.is_vacuumcmd); + return _j; }, vacuumRelation(_p?: VacuumRelation): VacuumRelation { const _j = {} as VacuumRelation; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "oid", _p?.oid); _o.set(_j, "va_cols", _p?.va_cols); + return _j; }, explainStmt(_p?: ExplainStmt): ExplainStmt { const _j = {} as ExplainStmt; + _o.set(_j, "query", _p?.query); _o.set(_j, "options", _p?.options); + return _j; }, createTableAsStmt(_p?: CreateTableAsStmt): CreateTableAsStmt { const _j = {} as CreateTableAsStmt; + _o.set(_j, "query", _p?.query); _o.set(_j, "into", _p?.into); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "is_select_into", _p?.is_select_into); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return _j; }, refreshMatViewStmt(_p?: RefreshMatViewStmt): RefreshMatViewStmt { const _j = {} as RefreshMatViewStmt; + _o.set(_j, "concurrent", _p?.concurrent); _o.set(_j, "skipData", _p?.skipData); _o.set(_j, "relation", _p?.relation); + return _j; }, checkPointStmt(_p?: CheckPointStmt): CheckPointStmt { const _j = {} as CheckPointStmt; + return _j; }, discardStmt(_p?: DiscardStmt): DiscardStmt { const _j = {} as DiscardStmt; + _o.set(_j, "target", _p?.target); + return _j; }, lockStmt(_p?: LockStmt): LockStmt { const _j = {} as LockStmt; + _o.set(_j, "relations", _p?.relations); _o.set(_j, "mode", _p?.mode); _o.set(_j, "nowait", _p?.nowait); + return _j; }, constraintsSetStmt(_p?: ConstraintsSetStmt): ConstraintsSetStmt { const _j = {} as ConstraintsSetStmt; + _o.set(_j, "constraints", _p?.constraints); _o.set(_j, "deferred", _p?.deferred); + return _j; }, reindexStmt(_p?: ReindexStmt): ReindexStmt { const _j = {} as ReindexStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "relation", _p?.relation); _o.set(_j, "name", _p?.name); _o.set(_j, "params", _p?.params); + return _j; }, createConversionStmt(_p?: CreateConversionStmt): CreateConversionStmt { const _j = {} as CreateConversionStmt; + _o.set(_j, "conversion_name", _p?.conversion_name); _o.set(_j, "for_encoding_name", _p?.for_encoding_name); _o.set(_j, "to_encoding_name", _p?.to_encoding_name); _o.set(_j, "func_name", _p?.func_name); _o.set(_j, "def", _p?.def); + return _j; }, createCastStmt(_p?: CreateCastStmt): CreateCastStmt { const _j = {} as CreateCastStmt; + _o.set(_j, "sourcetype", _p?.sourcetype); _o.set(_j, "targettype", _p?.targettype); _o.set(_j, "func", _p?.func); _o.set(_j, "context", _p?.context); _o.set(_j, "inout", _p?.inout); + return _j; }, createTransformStmt(_p?: CreateTransformStmt): CreateTransformStmt { const _j = {} as CreateTransformStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "type_name", _p?.type_name); _o.set(_j, "lang", _p?.lang); _o.set(_j, "fromsql", _p?.fromsql); _o.set(_j, "tosql", _p?.tosql); + return _j; }, prepareStmt(_p?: PrepareStmt): PrepareStmt { const _j = {} as PrepareStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "argtypes", _p?.argtypes); _o.set(_j, "query", _p?.query); + return _j; }, executeStmt(_p?: ExecuteStmt): ExecuteStmt { const _j = {} as ExecuteStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "params", _p?.params); + return _j; }, deallocateStmt(_p?: DeallocateStmt): DeallocateStmt { const _j = {} as DeallocateStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "isall", _p?.isall); _o.set(_j, "location", _p?.location); + return _j; }, dropOwnedStmt(_p?: DropOwnedStmt): DropOwnedStmt { const _j = {} as DropOwnedStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "behavior", _p?.behavior); + return _j; }, reassignOwnedStmt(_p?: ReassignOwnedStmt): ReassignOwnedStmt { const _j = {} as ReassignOwnedStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "newrole", _p?.newrole); + return _j; }, altertsDictionaryStmt(_p?: AlterTSDictionaryStmt): AlterTSDictionaryStmt { const _j = {} as AlterTSDictionaryStmt; + _o.set(_j, "dictname", _p?.dictname); _o.set(_j, "options", _p?.options); + return _j; }, altertsConfigurationStmt(_p?: AlterTSConfigurationStmt): AlterTSConfigurationStmt { const _j = {} as AlterTSConfigurationStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "cfgname", _p?.cfgname); _o.set(_j, "tokentype", _p?.tokentype); @@ -2421,70 +2943,87 @@ export default { _o.set(_j, "override", _p?.override); _o.set(_j, "replace", _p?.replace); _o.set(_j, "missing_ok", _p?.missing_ok); + return _j; }, publicationTable(_p?: PublicationTable): PublicationTable { const _j = {} as PublicationTable; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "columns", _p?.columns); + return _j; }, publicationObjSpec(_p?: PublicationObjSpec): PublicationObjSpec { const _j = {} as PublicationObjSpec; + _o.set(_j, "pubobjtype", _p?.pubobjtype); _o.set(_j, "name", _p?.name); _o.set(_j, "pubtable", _p?.pubtable); _o.set(_j, "location", _p?.location); + return _j; }, createPublicationStmt(_p?: CreatePublicationStmt): CreatePublicationStmt { const _j = {} as CreatePublicationStmt; + _o.set(_j, "pubname", _p?.pubname); _o.set(_j, "options", _p?.options); _o.set(_j, "pubobjects", _p?.pubobjects); _o.set(_j, "for_all_tables", _p?.for_all_tables); + return _j; }, alterPublicationStmt(_p?: AlterPublicationStmt): AlterPublicationStmt { const _j = {} as AlterPublicationStmt; + _o.set(_j, "pubname", _p?.pubname); _o.set(_j, "options", _p?.options); _o.set(_j, "pubobjects", _p?.pubobjects); _o.set(_j, "for_all_tables", _p?.for_all_tables); _o.set(_j, "action", _p?.action); + return _j; }, createSubscriptionStmt(_p?: CreateSubscriptionStmt): CreateSubscriptionStmt { const _j = {} as CreateSubscriptionStmt; + _o.set(_j, "subname", _p?.subname); _o.set(_j, "conninfo", _p?.conninfo); _o.set(_j, "publication", _p?.publication); _o.set(_j, "options", _p?.options); + return _j; }, alterSubscriptionStmt(_p?: AlterSubscriptionStmt): AlterSubscriptionStmt { const _j = {} as AlterSubscriptionStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "subname", _p?.subname); _o.set(_j, "conninfo", _p?.conninfo); _o.set(_j, "publication", _p?.publication); _o.set(_j, "options", _p?.options); + return _j; }, dropSubscriptionStmt(_p?: DropSubscriptionStmt): DropSubscriptionStmt { const _j = {} as DropSubscriptionStmt; + _o.set(_j, "subname", _p?.subname); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "behavior", _p?.behavior); + return _j; }, scanToken(_p?: ScanToken): ScanToken { const _j = {} as ScanToken; + _o.set(_j, "start", _p?.start); _o.set(_j, "end", _p?.end); _o.set(_j, "token", _p?.token); _o.set(_j, "keywordKind", _p?.keywordKind); + return _j; } }; \ No newline at end of file diff --git a/postgres/pg-ast/src/wrapped.ts b/postgres/pg-ast/src/wrapped.ts index a8bff0279..f9994db50 100644 --- a/postgres/pg-ast/src/wrapped.ts +++ b/postgres/pg-ast/src/wrapped.ts @@ -3,15 +3,17 @@ * DO NOT MODIFY IT BY HAND. Instead, modify the source proto file, * and run the pg-proto-parser generate command to regenerate this file. */ +import { A_ArrayExpr, A_Const, A_Expr, A_Indices, A_Indirection, A_Star, AccessPriv, Aggref, Alias, AlterCollationStmt, AlterDatabaseRefreshCollStmt, AlterDatabaseSetStmt, AlterDatabaseStmt, AlterDefaultPrivilegesStmt, AlterDomainStmt, AlterEnumStmt, AlterEventTrigStmt, AlterExtensionContentsStmt, AlterExtensionStmt, AlterFdwStmt, AlterForeignServerStmt, AlterFunctionStmt, AlternativeSubPlan, AlterObjectDependsStmt, AlterObjectSchemaStmt, AlterOperatorStmt, AlterOpFamilyStmt, AlterOwnerStmt, AlterPolicyStmt, AlterPublicationStmt, AlterRoleSetStmt, AlterRoleStmt, AlterSeqStmt, AlterStatsStmt, AlterSubscriptionStmt, AlterSystemStmt, AlterTableCmd, AlterTableMoveAllStmt, AlterTableSpaceOptionsStmt, AlterTableStmt, AlterTSConfigurationStmt, AlterTSDictionaryStmt, AlterTypeStmt, AlterUserMappingStmt, ArrayCoerceExpr, ArrayExpr, BitString, Boolean, BooleanTest, BoolExpr, CallContext, CallStmt, CaseExpr, CaseTestExpr, CaseWhen, CheckPointStmt, ClosePortalStmt, ClusterStmt, CoalesceExpr, CoerceToDomain, CoerceToDomainValue, CoerceViaIO, CollateClause, CollateExpr, ColumnDef, ColumnRef, CommentStmt, CommonTableExpr, CompositeTypeStmt, Constraint, ConstraintsSetStmt, ConvertRowtypeExpr, CopyStmt, CreateAmStmt, CreateCastStmt, CreateConversionStmt, CreatedbStmt, CreateDomainStmt, CreateEnumStmt, CreateEventTrigStmt, CreateExtensionStmt, CreateFdwStmt, CreateForeignServerStmt, CreateForeignTableStmt, CreateFunctionStmt, CreateOpClassItem, CreateOpClassStmt, CreateOpFamilyStmt, CreatePLangStmt, CreatePolicyStmt, CreatePublicationStmt, CreateRangeStmt, CreateRoleStmt, CreateSchemaStmt, CreateSeqStmt, CreateStatsStmt, CreateStmt, CreateSubscriptionStmt, CreateTableAsStmt, CreateTableSpaceStmt, CreateTransformStmt, CreateTrigStmt, CreateUserMappingStmt, CTECycleClause, CTESearchClause, CurrentOfExpr, DeallocateStmt, DeclareCursorStmt, DefElem, DefineStmt, DeleteStmt, DiscardStmt, DistinctExpr, DoStmt, DropdbStmt, DropOwnedStmt, DropRoleStmt, DropStmt, DropSubscriptionStmt, DropTableSpaceStmt, DropUserMappingStmt, ExecuteStmt, ExplainStmt, FetchStmt, FieldSelect, FieldStore, Float, FromExpr, FuncCall, FuncExpr, FunctionParameter, GrantRoleStmt, GrantStmt, GroupingFunc, GroupingSet, ImportForeignSchemaStmt, IndexElem, IndexStmt, InferClause, InferenceElem, InlineCodeBlock, InsertStmt, Integer, IntList, IntoClause, JoinExpr, JsonAggConstructor, JsonArgument, JsonArrayAgg, JsonArrayConstructor, JsonArrayQueryConstructor, JsonBehavior, JsonConstructorExpr, JsonExpr, JsonFormat, JsonFuncExpr, JsonIsPredicate, JsonKeyValue, JsonObjectAgg, JsonObjectConstructor, JsonOutput, JsonParseExpr, JsonReturning, JsonScalarExpr, JsonSerializeExpr, JsonTable, JsonTableColumn, JsonTablePath, JsonTablePathScan, JsonTablePathSpec, JsonTableSiblingJoin, JsonValueExpr, List, ListenStmt, LoadStmt, LockingClause, LockStmt, MergeAction, MergeStmt, MergeSupportFunc, MergeWhenClause, MinMaxExpr, MultiAssignRef, NamedArgExpr, NextValueExpr, NotifyStmt, NullIfExpr, NullTest, ObjectWithArgs, OidList, OnConflictClause, OnConflictExpr, OpExpr, Param, ParamRef, ParseResult, PartitionBoundSpec, PartitionCmd, PartitionElem, PartitionRangeDatum, PartitionSpec, PLAssignStmt, PrepareStmt, PublicationObjSpec, PublicationTable, Query, RangeFunction, RangeSubselect, RangeTableFunc, RangeTableFuncCol, RangeTableSample, RangeTblEntry, RangeTblFunction, RangeTblRef, RangeVar, RawStmt, ReassignOwnedStmt, RefreshMatViewStmt, ReindexStmt, RelabelType, RenameStmt, ReplicaIdentityStmt, ResTarget, ReturnStmt, RoleSpec, RowCompareExpr, RowExpr, RowMarkClause, RTEPermissionInfo, RuleStmt, ScalarArrayOpExpr, ScanResult, ScanToken,SecLabelStmt, SelectStmt, SetOperationStmt, SetToDefault, SinglePartitionSpec, SortBy, SortGroupClause, SQLValueFunction, StatsElem, String, SubLink, SubPlan, SubscriptingRef, TableFunc, TableLikeClause, TableSampleClause, TargetEntry, TransactionStmt, TriggerTransition, TruncateStmt, TypeCast, TypeName, UnlistenStmt, UpdateStmt, VacuumRelation, VacuumStmt, Var, VariableSetStmt, VariableShowStmt, ViewStmt, WindowClause, WindowDef, WindowFunc, WindowFuncRunCondition, WithCheckOption, WithClause, XmlExpr, XmlSerialize } from "@pgsql/types"; import _o from "nested-obj"; -import { ParseResult, ScanResult, Integer, Float, Boolean, String, BitString, List, OidList, IntList, A_Const, Alias, RangeVar, TableFunc, IntoClause, Var, Param, Aggref, GroupingFunc, WindowFunc, WindowFuncRunCondition, MergeSupportFunc, SubscriptingRef, FuncExpr, NamedArgExpr, OpExpr, DistinctExpr, NullIfExpr, ScalarArrayOpExpr, BoolExpr, SubLink, SubPlan, AlternativeSubPlan, FieldSelect, FieldStore, RelabelType, CoerceViaIO, ArrayCoerceExpr, ConvertRowtypeExpr, CollateExpr, CaseExpr, CaseWhen, CaseTestExpr, ArrayExpr, RowExpr, RowCompareExpr, CoalesceExpr, MinMaxExpr, SQLValueFunction, XmlExpr, JsonFormat, JsonReturning, JsonValueExpr, JsonConstructorExpr, JsonIsPredicate, JsonBehavior, JsonExpr, JsonTablePath, JsonTablePathScan, JsonTableSiblingJoin, NullTest, BooleanTest, MergeAction, CoerceToDomain, CoerceToDomainValue, SetToDefault, CurrentOfExpr, NextValueExpr, InferenceElem, TargetEntry, RangeTblRef, JoinExpr, FromExpr, OnConflictExpr, Query, TypeName, ColumnRef, ParamRef, A_Expr, TypeCast, CollateClause, RoleSpec, FuncCall, A_Star, A_Indices, A_Indirection, A_ArrayExpr, ResTarget, MultiAssignRef, SortBy, WindowDef, RangeSubselect, RangeFunction, RangeTableFunc, RangeTableFuncCol, RangeTableSample, ColumnDef, TableLikeClause, IndexElem, DefElem, LockingClause, XmlSerialize, PartitionElem, PartitionSpec, PartitionBoundSpec, PartitionRangeDatum, SinglePartitionSpec, PartitionCmd, RangeTblEntry, RTEPermissionInfo, RangeTblFunction, TableSampleClause, WithCheckOption, SortGroupClause, GroupingSet, WindowClause, RowMarkClause, WithClause, InferClause, OnConflictClause, CTESearchClause, CTECycleClause, CommonTableExpr, MergeWhenClause, TriggerTransition, JsonOutput, JsonArgument, JsonFuncExpr, JsonTablePathSpec, JsonTable, JsonTableColumn, JsonKeyValue, JsonParseExpr, JsonScalarExpr, JsonSerializeExpr, JsonObjectConstructor, JsonArrayConstructor, JsonArrayQueryConstructor, JsonAggConstructor, JsonObjectAgg, JsonArrayAgg, RawStmt, InsertStmt, DeleteStmt, UpdateStmt, MergeStmt, SelectStmt, SetOperationStmt, ReturnStmt, PLAssignStmt, CreateSchemaStmt, AlterTableStmt, ReplicaIdentityStmt, AlterTableCmd, AlterCollationStmt, AlterDomainStmt, GrantStmt, ObjectWithArgs, AccessPriv, GrantRoleStmt, AlterDefaultPrivilegesStmt, CopyStmt, VariableSetStmt, VariableShowStmt, CreateStmt, Constraint, CreateTableSpaceStmt, DropTableSpaceStmt, AlterTableSpaceOptionsStmt, AlterTableMoveAllStmt, CreateExtensionStmt, AlterExtensionStmt, AlterExtensionContentsStmt, CreateFdwStmt, AlterFdwStmt, CreateForeignServerStmt, AlterForeignServerStmt, CreateForeignTableStmt, CreateUserMappingStmt, AlterUserMappingStmt, DropUserMappingStmt, ImportForeignSchemaStmt, CreatePolicyStmt, AlterPolicyStmt, CreateAmStmt, CreateTrigStmt, CreateEventTrigStmt, AlterEventTrigStmt, CreatePLangStmt, CreateRoleStmt, AlterRoleStmt, AlterRoleSetStmt, DropRoleStmt, CreateSeqStmt, AlterSeqStmt, DefineStmt, CreateDomainStmt, CreateOpClassStmt, CreateOpClassItem, CreateOpFamilyStmt, AlterOpFamilyStmt, DropStmt, TruncateStmt, CommentStmt, SecLabelStmt, DeclareCursorStmt, ClosePortalStmt, FetchStmt, IndexStmt, CreateStatsStmt, StatsElem, AlterStatsStmt, CreateFunctionStmt, FunctionParameter, AlterFunctionStmt, DoStmt, InlineCodeBlock, CallStmt, CallContext, RenameStmt, AlterObjectDependsStmt, AlterObjectSchemaStmt, AlterOwnerStmt, AlterOperatorStmt, AlterTypeStmt, RuleStmt, NotifyStmt, ListenStmt, UnlistenStmt, TransactionStmt, CompositeTypeStmt, CreateEnumStmt, CreateRangeStmt, AlterEnumStmt, ViewStmt, LoadStmt, CreatedbStmt, AlterDatabaseStmt, AlterDatabaseRefreshCollStmt, AlterDatabaseSetStmt, DropdbStmt, AlterSystemStmt, ClusterStmt, VacuumStmt, VacuumRelation, ExplainStmt, CreateTableAsStmt, RefreshMatViewStmt, CheckPointStmt, DiscardStmt, LockStmt, ConstraintsSetStmt, ReindexStmt, CreateConversionStmt, CreateCastStmt, CreateTransformStmt, PrepareStmt, ExecuteStmt, DeallocateStmt, DropOwnedStmt, ReassignOwnedStmt, AlterTSDictionaryStmt, AlterTSConfigurationStmt, PublicationTable, PublicationObjSpec, CreatePublicationStmt, AlterPublicationStmt, CreateSubscriptionStmt, AlterSubscriptionStmt, DropSubscriptionStmt, ScanToken } from "@pgsql/types"; export default { parseResult(_p?: ParseResult): { ParseResult: ParseResult; } { const _j = {} as ParseResult; + _o.set(_j, "version", _p?.version); _o.set(_j, "stmts", _p?.stmts); + return { ParseResult: _j }; @@ -20,8 +22,10 @@ export default { ScanResult: ScanResult; } { const _j = {} as ScanResult; + _o.set(_j, "version", _p?.version); _o.set(_j, "tokens", _p?.tokens); + return { ScanResult: _j }; @@ -30,7 +34,9 @@ export default { Integer: Integer; } { const _j = {} as Integer; + _o.set(_j, "ival", _p?.ival); + return { Integer: _j }; @@ -39,7 +45,9 @@ export default { Float: Float; } { const _j = {} as Float; + _o.set(_j, "fval", _p?.fval); + return { Float: _j }; @@ -48,7 +56,9 @@ export default { Boolean: Boolean; } { const _j = {} as Boolean; + _o.set(_j, "boolval", _p?.boolval); + return { Boolean: _j }; @@ -57,7 +67,9 @@ export default { String: String; } { const _j = {} as String; + _o.set(_j, "sval", _p?.sval); + return { String: _j }; @@ -66,7 +78,9 @@ export default { BitString: BitString; } { const _j = {} as BitString; + _o.set(_j, "bsval", _p?.bsval); + return { BitString: _j }; @@ -75,7 +89,9 @@ export default { List: List; } { const _j = {} as List; + _o.set(_j, "items", _p?.items); + return { List: _j }; @@ -84,7 +100,9 @@ export default { OidList: OidList; } { const _j = {} as OidList; + _o.set(_j, "items", _p?.items); + return { OidList: _j }; @@ -93,7 +111,9 @@ export default { IntList: IntList; } { const _j = {} as IntList; + _o.set(_j, "items", _p?.items); + return { IntList: _j }; @@ -102,6 +122,7 @@ export default { A_Const: A_Const; } { const _j = {} as A_Const; + _o.set(_j, "ival", _p?.ival); _o.set(_j, "fval", _p?.fval); _o.set(_j, "boolval", _p?.boolval); @@ -109,6 +130,7 @@ export default { _o.set(_j, "bsval", _p?.bsval); _o.set(_j, "isnull", _p?.isnull); _o.set(_j, "location", _p?.location); + return { A_Const: _j }; @@ -117,8 +139,10 @@ export default { Alias: Alias; } { const _j = {} as Alias; + _o.set(_j, "aliasname", _p?.aliasname); _o.set(_j, "colnames", _p?.colnames); + return { Alias: _j }; @@ -127,6 +151,7 @@ export default { RangeVar: RangeVar; } { const _j = {} as RangeVar; + _o.set(_j, "catalogname", _p?.catalogname); _o.set(_j, "schemaname", _p?.schemaname); _o.set(_j, "relname", _p?.relname); @@ -134,6 +159,7 @@ export default { _o.set(_j, "relpersistence", _p?.relpersistence); _o.set(_j, "alias", _p?.alias); _o.set(_j, "location", _p?.location); + return { RangeVar: _j }; @@ -142,6 +168,7 @@ export default { TableFunc: TableFunc; } { const _j = {} as TableFunc; + _o.set(_j, "functype", _p?.functype); _o.set(_j, "ns_uris", _p?.ns_uris); _o.set(_j, "ns_names", _p?.ns_names); @@ -159,6 +186,7 @@ export default { _o.set(_j, "plan", _p?.plan); _o.set(_j, "ordinalitycol", _p?.ordinalitycol); _o.set(_j, "location", _p?.location); + return { TableFunc: _j }; @@ -167,6 +195,7 @@ export default { IntoClause: IntoClause; } { const _j = {} as IntoClause; + _o.set(_j, "rel", _p?.rel); _o.set(_j, "colNames", _p?.colNames); _o.set(_j, "accessMethod", _p?.accessMethod); @@ -175,6 +204,7 @@ export default { _o.set(_j, "tableSpaceName", _p?.tableSpaceName); _o.set(_j, "viewQuery", _p?.viewQuery); _o.set(_j, "skipData", _p?.skipData); + return { IntoClause: _j }; @@ -183,6 +213,7 @@ export default { Var: Var; } { const _j = {} as Var; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "varno", _p?.varno); _o.set(_j, "varattno", _p?.varattno); @@ -192,6 +223,7 @@ export default { _o.set(_j, "varnullingrels", _p?.varnullingrels); _o.set(_j, "varlevelsup", _p?.varlevelsup); _o.set(_j, "location", _p?.location); + return { Var: _j }; @@ -200,6 +232,7 @@ export default { Param: Param; } { const _j = {} as Param; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "paramkind", _p?.paramkind); _o.set(_j, "paramid", _p?.paramid); @@ -207,6 +240,7 @@ export default { _o.set(_j, "paramtypmod", _p?.paramtypmod); _o.set(_j, "paramcollid", _p?.paramcollid); _o.set(_j, "location", _p?.location); + return { Param: _j }; @@ -215,6 +249,7 @@ export default { Aggref: Aggref; } { const _j = {} as Aggref; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "aggfnoid", _p?.aggfnoid); _o.set(_j, "aggtype", _p?.aggtype); @@ -234,6 +269,7 @@ export default { _o.set(_j, "aggno", _p?.aggno); _o.set(_j, "aggtransno", _p?.aggtransno); _o.set(_j, "location", _p?.location); + return { Aggref: _j }; @@ -242,11 +278,13 @@ export default { GroupingFunc: GroupingFunc; } { const _j = {} as GroupingFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "args", _p?.args); _o.set(_j, "refs", _p?.refs); _o.set(_j, "agglevelsup", _p?.agglevelsup); _o.set(_j, "location", _p?.location); + return { GroupingFunc: _j }; @@ -255,6 +293,7 @@ export default { WindowFunc: WindowFunc; } { const _j = {} as WindowFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "winfnoid", _p?.winfnoid); _o.set(_j, "wintype", _p?.wintype); @@ -267,6 +306,7 @@ export default { _o.set(_j, "winstar", _p?.winstar); _o.set(_j, "winagg", _p?.winagg); _o.set(_j, "location", _p?.location); + return { WindowFunc: _j }; @@ -275,11 +315,13 @@ export default { WindowFuncRunCondition: WindowFuncRunCondition; } { const _j = {} as WindowFuncRunCondition; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "wfunc_left", _p?.wfunc_left); _o.set(_j, "arg", _p?.arg); + return { WindowFuncRunCondition: _j }; @@ -288,10 +330,12 @@ export default { MergeSupportFunc: MergeSupportFunc; } { const _j = {} as MergeSupportFunc; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "msftype", _p?.msftype); _o.set(_j, "msfcollid", _p?.msfcollid); _o.set(_j, "location", _p?.location); + return { MergeSupportFunc: _j }; @@ -300,6 +344,7 @@ export default { SubscriptingRef: SubscriptingRef; } { const _j = {} as SubscriptingRef; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "refcontainertype", _p?.refcontainertype); _o.set(_j, "refelemtype", _p?.refelemtype); @@ -310,6 +355,7 @@ export default { _o.set(_j, "reflowerindexpr", _p?.reflowerindexpr); _o.set(_j, "refexpr", _p?.refexpr); _o.set(_j, "refassgnexpr", _p?.refassgnexpr); + return { SubscriptingRef: _j }; @@ -318,6 +364,7 @@ export default { FuncExpr: FuncExpr; } { const _j = {} as FuncExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "funcid", _p?.funcid); _o.set(_j, "funcresulttype", _p?.funcresulttype); @@ -328,6 +375,7 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { FuncExpr: _j }; @@ -336,11 +384,13 @@ export default { NamedArgExpr: NamedArgExpr; } { const _j = {} as NamedArgExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "name", _p?.name); _o.set(_j, "argnumber", _p?.argnumber); _o.set(_j, "location", _p?.location); + return { NamedArgExpr: _j }; @@ -349,6 +399,7 @@ export default { OpExpr: OpExpr; } { const _j = {} as OpExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -357,6 +408,7 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { OpExpr: _j }; @@ -365,6 +417,7 @@ export default { DistinctExpr: DistinctExpr; } { const _j = {} as DistinctExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -373,6 +426,7 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { DistinctExpr: _j }; @@ -381,6 +435,7 @@ export default { NullIfExpr: NullIfExpr; } { const _j = {} as NullIfExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "opresulttype", _p?.opresulttype); @@ -389,6 +444,7 @@ export default { _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { NullIfExpr: _j }; @@ -397,12 +453,14 @@ export default { ScalarArrayOpExpr: ScalarArrayOpExpr; } { const _j = {} as ScalarArrayOpExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "opno", _p?.opno); _o.set(_j, "useOr", _p?.useOr); _o.set(_j, "inputcollid", _p?.inputcollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { ScalarArrayOpExpr: _j }; @@ -411,10 +469,12 @@ export default { BoolExpr: BoolExpr; } { const _j = {} as BoolExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "boolop", _p?.boolop); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { BoolExpr: _j }; @@ -423,6 +483,7 @@ export default { SubLink: SubLink; } { const _j = {} as SubLink; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subLinkType", _p?.subLinkType); _o.set(_j, "subLinkId", _p?.subLinkId); @@ -430,6 +491,7 @@ export default { _o.set(_j, "operName", _p?.operName); _o.set(_j, "subselect", _p?.subselect); _o.set(_j, "location", _p?.location); + return { SubLink: _j }; @@ -438,6 +500,7 @@ export default { SubPlan: SubPlan; } { const _j = {} as SubPlan; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subLinkType", _p?.subLinkType); _o.set(_j, "testexpr", _p?.testexpr); @@ -455,6 +518,7 @@ export default { _o.set(_j, "args", _p?.args); _o.set(_j, "startup_cost", _p?.startup_cost); _o.set(_j, "per_call_cost", _p?.per_call_cost); + return { SubPlan: _j }; @@ -463,8 +527,10 @@ export default { AlternativeSubPlan: AlternativeSubPlan; } { const _j = {} as AlternativeSubPlan; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "subplans", _p?.subplans); + return { AlternativeSubPlan: _j }; @@ -473,12 +539,14 @@ export default { FieldSelect: FieldSelect; } { const _j = {} as FieldSelect; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "fieldnum", _p?.fieldnum); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "resulttypmod", _p?.resulttypmod); _o.set(_j, "resultcollid", _p?.resultcollid); + return { FieldSelect: _j }; @@ -487,11 +555,13 @@ export default { FieldStore: FieldStore; } { const _j = {} as FieldStore; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "newvals", _p?.newvals); _o.set(_j, "fieldnums", _p?.fieldnums); _o.set(_j, "resulttype", _p?.resulttype); + return { FieldStore: _j }; @@ -500,6 +570,7 @@ export default { RelabelType: RelabelType; } { const _j = {} as RelabelType; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); @@ -507,6 +578,7 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "relabelformat", _p?.relabelformat); _o.set(_j, "location", _p?.location); + return { RelabelType: _j }; @@ -515,12 +587,14 @@ export default { CoerceViaIO: CoerceViaIO; } { const _j = {} as CoerceViaIO; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coerceformat", _p?.coerceformat); _o.set(_j, "location", _p?.location); + return { CoerceViaIO: _j }; @@ -529,6 +603,7 @@ export default { ArrayCoerceExpr: ArrayCoerceExpr; } { const _j = {} as ArrayCoerceExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "elemexpr", _p?.elemexpr); @@ -537,6 +612,7 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coerceformat", _p?.coerceformat); _o.set(_j, "location", _p?.location); + return { ArrayCoerceExpr: _j }; @@ -545,11 +621,13 @@ export default { ConvertRowtypeExpr: ConvertRowtypeExpr; } { const _j = {} as ConvertRowtypeExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); _o.set(_j, "convertformat", _p?.convertformat); _o.set(_j, "location", _p?.location); + return { ConvertRowtypeExpr: _j }; @@ -558,10 +636,12 @@ export default { CollateExpr: CollateExpr; } { const _j = {} as CollateExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "collOid", _p?.collOid); _o.set(_j, "location", _p?.location); + return { CollateExpr: _j }; @@ -570,6 +650,7 @@ export default { CaseExpr: CaseExpr; } { const _j = {} as CaseExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "casetype", _p?.casetype); _o.set(_j, "casecollid", _p?.casecollid); @@ -577,6 +658,7 @@ export default { _o.set(_j, "args", _p?.args); _o.set(_j, "defresult", _p?.defresult); _o.set(_j, "location", _p?.location); + return { CaseExpr: _j }; @@ -585,10 +667,12 @@ export default { CaseWhen: CaseWhen; } { const _j = {} as CaseWhen; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "result", _p?.result); _o.set(_j, "location", _p?.location); + return { CaseWhen: _j }; @@ -597,10 +681,12 @@ export default { CaseTestExpr: CaseTestExpr; } { const _j = {} as CaseTestExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); + return { CaseTestExpr: _j }; @@ -609,6 +695,7 @@ export default { ArrayExpr: ArrayExpr; } { const _j = {} as ArrayExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "array_typeid", _p?.array_typeid); _o.set(_j, "array_collid", _p?.array_collid); @@ -616,6 +703,7 @@ export default { _o.set(_j, "elements", _p?.elements); _o.set(_j, "multidims", _p?.multidims); _o.set(_j, "location", _p?.location); + return { ArrayExpr: _j }; @@ -624,12 +712,14 @@ export default { RowExpr: RowExpr; } { const _j = {} as RowExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "args", _p?.args); _o.set(_j, "row_typeid", _p?.row_typeid); _o.set(_j, "row_format", _p?.row_format); _o.set(_j, "colnames", _p?.colnames); _o.set(_j, "location", _p?.location); + return { RowExpr: _j }; @@ -638,6 +728,7 @@ export default { RowCompareExpr: RowCompareExpr; } { const _j = {} as RowCompareExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "rctype", _p?.rctype); _o.set(_j, "opnos", _p?.opnos); @@ -645,6 +736,7 @@ export default { _o.set(_j, "inputcollids", _p?.inputcollids); _o.set(_j, "largs", _p?.largs); _o.set(_j, "rargs", _p?.rargs); + return { RowCompareExpr: _j }; @@ -653,11 +745,13 @@ export default { CoalesceExpr: CoalesceExpr; } { const _j = {} as CoalesceExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "coalescetype", _p?.coalescetype); _o.set(_j, "coalescecollid", _p?.coalescecollid); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { CoalesceExpr: _j }; @@ -666,6 +760,7 @@ export default { MinMaxExpr: MinMaxExpr; } { const _j = {} as MinMaxExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "minmaxtype", _p?.minmaxtype); _o.set(_j, "minmaxcollid", _p?.minmaxcollid); @@ -673,6 +768,7 @@ export default { _o.set(_j, "op", _p?.op); _o.set(_j, "args", _p?.args); _o.set(_j, "location", _p?.location); + return { MinMaxExpr: _j }; @@ -681,11 +777,13 @@ export default { SQLValueFunction: SQLValueFunction; } { const _j = {} as SQLValueFunction; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "type", _p?.type); _o.set(_j, "typmod", _p?.typmod); _o.set(_j, "location", _p?.location); + return { SQLValueFunction: _j }; @@ -694,6 +792,7 @@ export default { XmlExpr: XmlExpr; } { const _j = {} as XmlExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "name", _p?.name); @@ -705,6 +804,7 @@ export default { _o.set(_j, "type", _p?.type); _o.set(_j, "typmod", _p?.typmod); _o.set(_j, "location", _p?.location); + return { XmlExpr: _j }; @@ -713,9 +813,11 @@ export default { JsonFormat: JsonFormat; } { const _j = {} as JsonFormat; + _o.set(_j, "format_type", _p?.format_type); _o.set(_j, "encoding", _p?.encoding); _o.set(_j, "location", _p?.location); + return { JsonFormat: _j }; @@ -724,9 +826,11 @@ export default { JsonReturning: JsonReturning; } { const _j = {} as JsonReturning; + _o.set(_j, "format", _p?.format); _o.set(_j, "typid", _p?.typid); _o.set(_j, "typmod", _p?.typmod); + return { JsonReturning: _j }; @@ -735,9 +839,11 @@ export default { JsonValueExpr: JsonValueExpr; } { const _j = {} as JsonValueExpr; + _o.set(_j, "raw_expr", _p?.raw_expr); _o.set(_j, "formatted_expr", _p?.formatted_expr); _o.set(_j, "format", _p?.format); + return { JsonValueExpr: _j }; @@ -746,6 +852,7 @@ export default { JsonConstructorExpr: JsonConstructorExpr; } { const _j = {} as JsonConstructorExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "type", _p?.type); _o.set(_j, "args", _p?.args); @@ -755,6 +862,7 @@ export default { _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); _o.set(_j, "location", _p?.location); + return { JsonConstructorExpr: _j }; @@ -763,11 +871,13 @@ export default { JsonIsPredicate: JsonIsPredicate; } { const _j = {} as JsonIsPredicate; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "format", _p?.format); _o.set(_j, "item_type", _p?.item_type); _o.set(_j, "unique_keys", _p?.unique_keys); _o.set(_j, "location", _p?.location); + return { JsonIsPredicate: _j }; @@ -776,10 +886,12 @@ export default { JsonBehavior: JsonBehavior; } { const _j = {} as JsonBehavior; + _o.set(_j, "btype", _p?.btype); _o.set(_j, "expr", _p?.expr); _o.set(_j, "coerce", _p?.coerce); _o.set(_j, "location", _p?.location); + return { JsonBehavior: _j }; @@ -788,6 +900,7 @@ export default { JsonExpr: JsonExpr; } { const _j = {} as JsonExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "op", _p?.op); _o.set(_j, "column_name", _p?.column_name); @@ -805,6 +918,7 @@ export default { _o.set(_j, "omit_quotes", _p?.omit_quotes); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return { JsonExpr: _j }; @@ -813,7 +927,9 @@ export default { JsonTablePath: JsonTablePath; } { const _j = {} as JsonTablePath; + _o.set(_j, "name", _p?.name); + return { JsonTablePath: _j }; @@ -822,12 +938,14 @@ export default { JsonTablePathScan: JsonTablePathScan; } { const _j = {} as JsonTablePathScan; + _o.set(_j, "plan", _p?.plan); _o.set(_j, "path", _p?.path); _o.set(_j, "errorOnError", _p?.errorOnError); _o.set(_j, "child", _p?.child); _o.set(_j, "colMin", _p?.colMin); _o.set(_j, "colMax", _p?.colMax); + return { JsonTablePathScan: _j }; @@ -836,9 +954,11 @@ export default { JsonTableSiblingJoin: JsonTableSiblingJoin; } { const _j = {} as JsonTableSiblingJoin; + _o.set(_j, "plan", _p?.plan); _o.set(_j, "lplan", _p?.lplan); _o.set(_j, "rplan", _p?.rplan); + return { JsonTableSiblingJoin: _j }; @@ -847,11 +967,13 @@ export default { NullTest: NullTest; } { const _j = {} as NullTest; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "nulltesttype", _p?.nulltesttype); _o.set(_j, "argisrow", _p?.argisrow); _o.set(_j, "location", _p?.location); + return { NullTest: _j }; @@ -860,10 +982,12 @@ export default { BooleanTest: BooleanTest; } { const _j = {} as BooleanTest; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "booltesttype", _p?.booltesttype); _o.set(_j, "location", _p?.location); + return { BooleanTest: _j }; @@ -872,12 +996,14 @@ export default { MergeAction: MergeAction; } { const _j = {} as MergeAction; + _o.set(_j, "matchKind", _p?.matchKind); _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "override", _p?.override); _o.set(_j, "qual", _p?.qual); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "updateColnos", _p?.updateColnos); + return { MergeAction: _j }; @@ -886,6 +1012,7 @@ export default { CoerceToDomain: CoerceToDomain; } { const _j = {} as CoerceToDomain; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "arg", _p?.arg); _o.set(_j, "resulttype", _p?.resulttype); @@ -893,6 +1020,7 @@ export default { _o.set(_j, "resultcollid", _p?.resultcollid); _o.set(_j, "coercionformat", _p?.coercionformat); _o.set(_j, "location", _p?.location); + return { CoerceToDomain: _j }; @@ -901,11 +1029,13 @@ export default { CoerceToDomainValue: CoerceToDomainValue; } { const _j = {} as CoerceToDomainValue; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return { CoerceToDomainValue: _j }; @@ -914,11 +1044,13 @@ export default { SetToDefault: SetToDefault; } { const _j = {} as SetToDefault; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "typeId", _p?.typeId); _o.set(_j, "typeMod", _p?.typeMod); _o.set(_j, "collation", _p?.collation); _o.set(_j, "location", _p?.location); + return { SetToDefault: _j }; @@ -927,10 +1059,12 @@ export default { CurrentOfExpr: CurrentOfExpr; } { const _j = {} as CurrentOfExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "cvarno", _p?.cvarno); _o.set(_j, "cursor_name", _p?.cursor_name); _o.set(_j, "cursor_param", _p?.cursor_param); + return { CurrentOfExpr: _j }; @@ -939,9 +1073,11 @@ export default { NextValueExpr: NextValueExpr; } { const _j = {} as NextValueExpr; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "seqid", _p?.seqid); _o.set(_j, "typeId", _p?.typeId); + return { NextValueExpr: _j }; @@ -950,10 +1086,12 @@ export default { InferenceElem: InferenceElem; } { const _j = {} as InferenceElem; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "infercollid", _p?.infercollid); _o.set(_j, "inferopclass", _p?.inferopclass); + return { InferenceElem: _j }; @@ -962,6 +1100,7 @@ export default { TargetEntry: TargetEntry; } { const _j = {} as TargetEntry; + _o.set(_j, "xpr", _p?.xpr); _o.set(_j, "expr", _p?.expr); _o.set(_j, "resno", _p?.resno); @@ -970,6 +1109,7 @@ export default { _o.set(_j, "resorigtbl", _p?.resorigtbl); _o.set(_j, "resorigcol", _p?.resorigcol); _o.set(_j, "resjunk", _p?.resjunk); + return { TargetEntry: _j }; @@ -978,7 +1118,9 @@ export default { RangeTblRef: RangeTblRef; } { const _j = {} as RangeTblRef; + _o.set(_j, "rtindex", _p?.rtindex); + return { RangeTblRef: _j }; @@ -987,6 +1129,7 @@ export default { JoinExpr: JoinExpr; } { const _j = {} as JoinExpr; + _o.set(_j, "jointype", _p?.jointype); _o.set(_j, "isNatural", _p?.isNatural); _o.set(_j, "larg", _p?.larg); @@ -996,6 +1139,7 @@ export default { _o.set(_j, "quals", _p?.quals); _o.set(_j, "alias", _p?.alias); _o.set(_j, "rtindex", _p?.rtindex); + return { JoinExpr: _j }; @@ -1004,8 +1148,10 @@ export default { FromExpr: FromExpr; } { const _j = {} as FromExpr; + _o.set(_j, "fromlist", _p?.fromlist); _o.set(_j, "quals", _p?.quals); + return { FromExpr: _j }; @@ -1014,6 +1160,7 @@ export default { OnConflictExpr: OnConflictExpr; } { const _j = {} as OnConflictExpr; + _o.set(_j, "action", _p?.action); _o.set(_j, "arbiterElems", _p?.arbiterElems); _o.set(_j, "arbiterWhere", _p?.arbiterWhere); @@ -1022,6 +1169,7 @@ export default { _o.set(_j, "onConflictWhere", _p?.onConflictWhere); _o.set(_j, "exclRelIndex", _p?.exclRelIndex); _o.set(_j, "exclRelTlist", _p?.exclRelTlist); + return { OnConflictExpr: _j }; @@ -1030,6 +1178,7 @@ export default { Query: Query; } { const _j = {} as Query; + _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "querySource", _p?.querySource); _o.set(_j, "canSetTag", _p?.canSetTag); @@ -1072,6 +1221,7 @@ export default { _o.set(_j, "withCheckOptions", _p?.withCheckOptions); _o.set(_j, "stmt_location", _p?.stmt_location); _o.set(_j, "stmt_len", _p?.stmt_len); + return { Query: _j }; @@ -1080,6 +1230,7 @@ export default { TypeName: TypeName; } { const _j = {} as TypeName; + _o.set(_j, "names", _p?.names); _o.set(_j, "typeOid", _p?.typeOid); _o.set(_j, "setof", _p?.setof); @@ -1088,6 +1239,7 @@ export default { _o.set(_j, "typemod", _p?.typemod); _o.set(_j, "arrayBounds", _p?.arrayBounds); _o.set(_j, "location", _p?.location); + return { TypeName: _j }; @@ -1096,8 +1248,10 @@ export default { ColumnRef: ColumnRef; } { const _j = {} as ColumnRef; + _o.set(_j, "fields", _p?.fields); _o.set(_j, "location", _p?.location); + return { ColumnRef: _j }; @@ -1106,8 +1260,10 @@ export default { ParamRef: ParamRef; } { const _j = {} as ParamRef; + _o.set(_j, "number", _p?.number); _o.set(_j, "location", _p?.location); + return { ParamRef: _j }; @@ -1116,11 +1272,13 @@ export default { A_Expr: A_Expr; } { const _j = {} as A_Expr; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "name", _p?.name); _o.set(_j, "lexpr", _p?.lexpr); _o.set(_j, "rexpr", _p?.rexpr); _o.set(_j, "location", _p?.location); + return { A_Expr: _j }; @@ -1129,9 +1287,11 @@ export default { TypeCast: TypeCast; } { const _j = {} as TypeCast; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "location", _p?.location); + return { TypeCast: _j }; @@ -1140,9 +1300,11 @@ export default { CollateClause: CollateClause; } { const _j = {} as CollateClause; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "collname", _p?.collname); _o.set(_j, "location", _p?.location); + return { CollateClause: _j }; @@ -1151,9 +1313,11 @@ export default { RoleSpec: RoleSpec; } { const _j = {} as RoleSpec; + _o.set(_j, "roletype", _p?.roletype); _o.set(_j, "rolename", _p?.rolename); _o.set(_j, "location", _p?.location); + return { RoleSpec: _j }; @@ -1162,6 +1326,7 @@ export default { FuncCall: FuncCall; } { const _j = {} as FuncCall; + _o.set(_j, "funcname", _p?.funcname); _o.set(_j, "args", _p?.args); _o.set(_j, "agg_order", _p?.agg_order); @@ -1173,6 +1338,7 @@ export default { _o.set(_j, "func_variadic", _p?.func_variadic); _o.set(_j, "funcformat", _p?.funcformat); _o.set(_j, "location", _p?.location); + return { FuncCall: _j }; @@ -1181,6 +1347,7 @@ export default { A_Star: A_Star; } { const _j = {} as A_Star; + return { A_Star: _j }; @@ -1189,9 +1356,11 @@ export default { A_Indices: A_Indices; } { const _j = {} as A_Indices; + _o.set(_j, "is_slice", _p?.is_slice); _o.set(_j, "lidx", _p?.lidx); _o.set(_j, "uidx", _p?.uidx); + return { A_Indices: _j }; @@ -1200,8 +1369,10 @@ export default { A_Indirection: A_Indirection; } { const _j = {} as A_Indirection; + _o.set(_j, "arg", _p?.arg); _o.set(_j, "indirection", _p?.indirection); + return { A_Indirection: _j }; @@ -1210,8 +1381,10 @@ export default { A_ArrayExpr: A_ArrayExpr; } { const _j = {} as A_ArrayExpr; + _o.set(_j, "elements", _p?.elements); _o.set(_j, "location", _p?.location); + return { A_ArrayExpr: _j }; @@ -1220,10 +1393,12 @@ export default { ResTarget: ResTarget; } { const _j = {} as ResTarget; + _o.set(_j, "name", _p?.name); _o.set(_j, "indirection", _p?.indirection); _o.set(_j, "val", _p?.val); _o.set(_j, "location", _p?.location); + return { ResTarget: _j }; @@ -1232,9 +1407,11 @@ export default { MultiAssignRef: MultiAssignRef; } { const _j = {} as MultiAssignRef; + _o.set(_j, "source", _p?.source); _o.set(_j, "colno", _p?.colno); _o.set(_j, "ncolumns", _p?.ncolumns); + return { MultiAssignRef: _j }; @@ -1243,11 +1420,13 @@ export default { SortBy: SortBy; } { const _j = {} as SortBy; + _o.set(_j, "node", _p?.node); _o.set(_j, "sortby_dir", _p?.sortby_dir); _o.set(_j, "sortby_nulls", _p?.sortby_nulls); _o.set(_j, "useOp", _p?.useOp); _o.set(_j, "location", _p?.location); + return { SortBy: _j }; @@ -1256,6 +1435,7 @@ export default { WindowDef: WindowDef; } { const _j = {} as WindowDef; + _o.set(_j, "name", _p?.name); _o.set(_j, "refname", _p?.refname); _o.set(_j, "partitionClause", _p?.partitionClause); @@ -1264,6 +1444,7 @@ export default { _o.set(_j, "startOffset", _p?.startOffset); _o.set(_j, "endOffset", _p?.endOffset); _o.set(_j, "location", _p?.location); + return { WindowDef: _j }; @@ -1272,9 +1453,11 @@ export default { RangeSubselect: RangeSubselect; } { const _j = {} as RangeSubselect; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "subquery", _p?.subquery); _o.set(_j, "alias", _p?.alias); + return { RangeSubselect: _j }; @@ -1283,12 +1466,14 @@ export default { RangeFunction: RangeFunction; } { const _j = {} as RangeFunction; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "ordinality", _p?.ordinality); _o.set(_j, "is_rowsfrom", _p?.is_rowsfrom); _o.set(_j, "functions", _p?.functions); _o.set(_j, "alias", _p?.alias); _o.set(_j, "coldeflist", _p?.coldeflist); + return { RangeFunction: _j }; @@ -1297,6 +1482,7 @@ export default { RangeTableFunc: RangeTableFunc; } { const _j = {} as RangeTableFunc; + _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "docexpr", _p?.docexpr); _o.set(_j, "rowexpr", _p?.rowexpr); @@ -1304,6 +1490,7 @@ export default { _o.set(_j, "columns", _p?.columns); _o.set(_j, "alias", _p?.alias); _o.set(_j, "location", _p?.location); + return { RangeTableFunc: _j }; @@ -1312,6 +1499,7 @@ export default { RangeTableFuncCol: RangeTableFuncCol; } { const _j = {} as RangeTableFuncCol; + _o.set(_j, "colname", _p?.colname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "for_ordinality", _p?.for_ordinality); @@ -1319,6 +1507,7 @@ export default { _o.set(_j, "colexpr", _p?.colexpr); _o.set(_j, "coldefexpr", _p?.coldefexpr); _o.set(_j, "location", _p?.location); + return { RangeTableFuncCol: _j }; @@ -1327,11 +1516,13 @@ export default { RangeTableSample: RangeTableSample; } { const _j = {} as RangeTableSample; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "method", _p?.method); _o.set(_j, "args", _p?.args); _o.set(_j, "repeatable", _p?.repeatable); _o.set(_j, "location", _p?.location); + return { RangeTableSample: _j }; @@ -1340,6 +1531,7 @@ export default { ColumnDef: ColumnDef; } { const _j = {} as ColumnDef; + _o.set(_j, "colname", _p?.colname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "compression", _p?.compression); @@ -1359,6 +1551,7 @@ export default { _o.set(_j, "constraints", _p?.constraints); _o.set(_j, "fdwoptions", _p?.fdwoptions); _o.set(_j, "location", _p?.location); + return { ColumnDef: _j }; @@ -1367,9 +1560,11 @@ export default { TableLikeClause: TableLikeClause; } { const _j = {} as TableLikeClause; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "options", _p?.options); _o.set(_j, "relationOid", _p?.relationOid); + return { TableLikeClause: _j }; @@ -1378,6 +1573,7 @@ export default { IndexElem: IndexElem; } { const _j = {} as IndexElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); _o.set(_j, "indexcolname", _p?.indexcolname); @@ -1386,6 +1582,7 @@ export default { _o.set(_j, "opclassopts", _p?.opclassopts); _o.set(_j, "ordering", _p?.ordering); _o.set(_j, "nulls_ordering", _p?.nulls_ordering); + return { IndexElem: _j }; @@ -1394,11 +1591,13 @@ export default { DefElem: DefElem; } { const _j = {} as DefElem; + _o.set(_j, "defnamespace", _p?.defnamespace); _o.set(_j, "defname", _p?.defname); _o.set(_j, "arg", _p?.arg); _o.set(_j, "defaction", _p?.defaction); _o.set(_j, "location", _p?.location); + return { DefElem: _j }; @@ -1407,9 +1606,11 @@ export default { LockingClause: LockingClause; } { const _j = {} as LockingClause; + _o.set(_j, "lockedRels", _p?.lockedRels); _o.set(_j, "strength", _p?.strength); _o.set(_j, "waitPolicy", _p?.waitPolicy); + return { LockingClause: _j }; @@ -1418,11 +1619,13 @@ export default { XmlSerialize: XmlSerialize; } { const _j = {} as XmlSerialize; + _o.set(_j, "xmloption", _p?.xmloption); _o.set(_j, "expr", _p?.expr); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "indent", _p?.indent); _o.set(_j, "location", _p?.location); + return { XmlSerialize: _j }; @@ -1431,11 +1634,13 @@ export default { PartitionElem: PartitionElem; } { const _j = {} as PartitionElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); _o.set(_j, "collation", _p?.collation); _o.set(_j, "opclass", _p?.opclass); _o.set(_j, "location", _p?.location); + return { PartitionElem: _j }; @@ -1444,9 +1649,11 @@ export default { PartitionSpec: PartitionSpec; } { const _j = {} as PartitionSpec; + _o.set(_j, "strategy", _p?.strategy); _o.set(_j, "partParams", _p?.partParams); _o.set(_j, "location", _p?.location); + return { PartitionSpec: _j }; @@ -1455,6 +1662,7 @@ export default { PartitionBoundSpec: PartitionBoundSpec; } { const _j = {} as PartitionBoundSpec; + _o.set(_j, "strategy", _p?.strategy); _o.set(_j, "is_default", _p?.is_default); _o.set(_j, "modulus", _p?.modulus); @@ -1463,6 +1671,7 @@ export default { _o.set(_j, "lowerdatums", _p?.lowerdatums); _o.set(_j, "upperdatums", _p?.upperdatums); _o.set(_j, "location", _p?.location); + return { PartitionBoundSpec: _j }; @@ -1471,9 +1680,11 @@ export default { PartitionRangeDatum: PartitionRangeDatum; } { const _j = {} as PartitionRangeDatum; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "value", _p?.value); _o.set(_j, "location", _p?.location); + return { PartitionRangeDatum: _j }; @@ -1482,6 +1693,7 @@ export default { SinglePartitionSpec: SinglePartitionSpec; } { const _j = {} as SinglePartitionSpec; + return { SinglePartitionSpec: _j }; @@ -1490,9 +1702,11 @@ export default { PartitionCmd: PartitionCmd; } { const _j = {} as PartitionCmd; + _o.set(_j, "name", _p?.name); _o.set(_j, "bound", _p?.bound); _o.set(_j, "concurrent", _p?.concurrent); + return { PartitionCmd: _j }; @@ -1501,6 +1715,7 @@ export default { RangeTblEntry: RangeTblEntry; } { const _j = {} as RangeTblEntry; + _o.set(_j, "alias", _p?.alias); _o.set(_j, "eref", _p?.eref); _o.set(_j, "rtekind", _p?.rtekind); @@ -1533,6 +1748,7 @@ export default { _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "inFromCl", _p?.inFromCl); _o.set(_j, "securityQuals", _p?.securityQuals); + return { RangeTblEntry: _j }; @@ -1541,6 +1757,7 @@ export default { RTEPermissionInfo: RTEPermissionInfo; } { const _j = {} as RTEPermissionInfo; + _o.set(_j, "relid", _p?.relid); _o.set(_j, "inh", _p?.inh); _o.set(_j, "requiredPerms", _p?.requiredPerms); @@ -1548,6 +1765,7 @@ export default { _o.set(_j, "selectedCols", _p?.selectedCols); _o.set(_j, "insertedCols", _p?.insertedCols); _o.set(_j, "updatedCols", _p?.updatedCols); + return { RTEPermissionInfo: _j }; @@ -1556,6 +1774,7 @@ export default { RangeTblFunction: RangeTblFunction; } { const _j = {} as RangeTblFunction; + _o.set(_j, "funcexpr", _p?.funcexpr); _o.set(_j, "funccolcount", _p?.funccolcount); _o.set(_j, "funccolnames", _p?.funccolnames); @@ -1563,6 +1782,7 @@ export default { _o.set(_j, "funccoltypmods", _p?.funccoltypmods); _o.set(_j, "funccolcollations", _p?.funccolcollations); _o.set(_j, "funcparams", _p?.funcparams); + return { RangeTblFunction: _j }; @@ -1571,9 +1791,11 @@ export default { TableSampleClause: TableSampleClause; } { const _j = {} as TableSampleClause; + _o.set(_j, "tsmhandler", _p?.tsmhandler); _o.set(_j, "args", _p?.args); _o.set(_j, "repeatable", _p?.repeatable); + return { TableSampleClause: _j }; @@ -1582,11 +1804,13 @@ export default { WithCheckOption: WithCheckOption; } { const _j = {} as WithCheckOption; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "relname", _p?.relname); _o.set(_j, "polname", _p?.polname); _o.set(_j, "qual", _p?.qual); _o.set(_j, "cascaded", _p?.cascaded); + return { WithCheckOption: _j }; @@ -1595,11 +1819,13 @@ export default { SortGroupClause: SortGroupClause; } { const _j = {} as SortGroupClause; + _o.set(_j, "tleSortGroupRef", _p?.tleSortGroupRef); _o.set(_j, "eqop", _p?.eqop); _o.set(_j, "sortop", _p?.sortop); _o.set(_j, "nulls_first", _p?.nulls_first); _o.set(_j, "hashable", _p?.hashable); + return { SortGroupClause: _j }; @@ -1608,9 +1834,11 @@ export default { GroupingSet: GroupingSet; } { const _j = {} as GroupingSet; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "content", _p?.content); _o.set(_j, "location", _p?.location); + return { GroupingSet: _j }; @@ -1619,6 +1847,7 @@ export default { WindowClause: WindowClause; } { const _j = {} as WindowClause; + _o.set(_j, "name", _p?.name); _o.set(_j, "refname", _p?.refname); _o.set(_j, "partitionClause", _p?.partitionClause); @@ -1633,6 +1862,7 @@ export default { _o.set(_j, "inRangeNullsFirst", _p?.inRangeNullsFirst); _o.set(_j, "winref", _p?.winref); _o.set(_j, "copiedOrder", _p?.copiedOrder); + return { WindowClause: _j }; @@ -1641,10 +1871,12 @@ export default { RowMarkClause: RowMarkClause; } { const _j = {} as RowMarkClause; + _o.set(_j, "rti", _p?.rti); _o.set(_j, "strength", _p?.strength); _o.set(_j, "waitPolicy", _p?.waitPolicy); _o.set(_j, "pushedDown", _p?.pushedDown); + return { RowMarkClause: _j }; @@ -1653,9 +1885,11 @@ export default { WithClause: WithClause; } { const _j = {} as WithClause; + _o.set(_j, "ctes", _p?.ctes); _o.set(_j, "recursive", _p?.recursive); _o.set(_j, "location", _p?.location); + return { WithClause: _j }; @@ -1664,10 +1898,12 @@ export default { InferClause: InferClause; } { const _j = {} as InferClause; + _o.set(_j, "indexElems", _p?.indexElems); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "conname", _p?.conname); _o.set(_j, "location", _p?.location); + return { InferClause: _j }; @@ -1676,11 +1912,13 @@ export default { OnConflictClause: OnConflictClause; } { const _j = {} as OnConflictClause; + _o.set(_j, "action", _p?.action); _o.set(_j, "infer", _p?.infer); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "location", _p?.location); + return { OnConflictClause: _j }; @@ -1689,10 +1927,12 @@ export default { CTESearchClause: CTESearchClause; } { const _j = {} as CTESearchClause; + _o.set(_j, "search_col_list", _p?.search_col_list); _o.set(_j, "search_breadth_first", _p?.search_breadth_first); _o.set(_j, "search_seq_column", _p?.search_seq_column); _o.set(_j, "location", _p?.location); + return { CTESearchClause: _j }; @@ -1701,6 +1941,7 @@ export default { CTECycleClause: CTECycleClause; } { const _j = {} as CTECycleClause; + _o.set(_j, "cycle_col_list", _p?.cycle_col_list); _o.set(_j, "cycle_mark_column", _p?.cycle_mark_column); _o.set(_j, "cycle_mark_value", _p?.cycle_mark_value); @@ -1711,6 +1952,7 @@ export default { _o.set(_j, "cycle_mark_typmod", _p?.cycle_mark_typmod); _o.set(_j, "cycle_mark_collation", _p?.cycle_mark_collation); _o.set(_j, "cycle_mark_neop", _p?.cycle_mark_neop); + return { CTECycleClause: _j }; @@ -1719,6 +1961,7 @@ export default { CommonTableExpr: CommonTableExpr; } { const _j = {} as CommonTableExpr; + _o.set(_j, "ctename", _p?.ctename); _o.set(_j, "aliascolnames", _p?.aliascolnames); _o.set(_j, "ctematerialized", _p?.ctematerialized); @@ -1732,6 +1975,7 @@ export default { _o.set(_j, "ctecoltypes", _p?.ctecoltypes); _o.set(_j, "ctecoltypmods", _p?.ctecoltypmods); _o.set(_j, "ctecolcollations", _p?.ctecolcollations); + return { CommonTableExpr: _j }; @@ -1740,12 +1984,14 @@ export default { MergeWhenClause: MergeWhenClause; } { const _j = {} as MergeWhenClause; + _o.set(_j, "matchKind", _p?.matchKind); _o.set(_j, "commandType", _p?.commandType); _o.set(_j, "override", _p?.override); _o.set(_j, "condition", _p?.condition); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "values", _p?.values); + return { MergeWhenClause: _j }; @@ -1754,9 +2000,11 @@ export default { TriggerTransition: TriggerTransition; } { const _j = {} as TriggerTransition; + _o.set(_j, "name", _p?.name); _o.set(_j, "isNew", _p?.isNew); _o.set(_j, "isTable", _p?.isTable); + return { TriggerTransition: _j }; @@ -1765,8 +2013,10 @@ export default { JsonOutput: JsonOutput; } { const _j = {} as JsonOutput; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "returning", _p?.returning); + return { JsonOutput: _j }; @@ -1775,8 +2025,10 @@ export default { JsonArgument: JsonArgument; } { const _j = {} as JsonArgument; + _o.set(_j, "val", _p?.val); _o.set(_j, "name", _p?.name); + return { JsonArgument: _j }; @@ -1785,6 +2037,7 @@ export default { JsonFuncExpr: JsonFuncExpr; } { const _j = {} as JsonFuncExpr; + _o.set(_j, "op", _p?.op); _o.set(_j, "column_name", _p?.column_name); _o.set(_j, "context_item", _p?.context_item); @@ -1796,6 +2049,7 @@ export default { _o.set(_j, "wrapper", _p?.wrapper); _o.set(_j, "quotes", _p?.quotes); _o.set(_j, "location", _p?.location); + return { JsonFuncExpr: _j }; @@ -1804,10 +2058,12 @@ export default { JsonTablePathSpec: JsonTablePathSpec; } { const _j = {} as JsonTablePathSpec; + _o.set(_j, "string", _p?.string); _o.set(_j, "name", _p?.name); _o.set(_j, "name_location", _p?.name_location); _o.set(_j, "location", _p?.location); + return { JsonTablePathSpec: _j }; @@ -1816,6 +2072,7 @@ export default { JsonTable: JsonTable; } { const _j = {} as JsonTable; + _o.set(_j, "context_item", _p?.context_item); _o.set(_j, "pathspec", _p?.pathspec); _o.set(_j, "passing", _p?.passing); @@ -1824,6 +2081,7 @@ export default { _o.set(_j, "alias", _p?.alias); _o.set(_j, "lateral", _p?.lateral); _o.set(_j, "location", _p?.location); + return { JsonTable: _j }; @@ -1832,6 +2090,7 @@ export default { JsonTableColumn: JsonTableColumn; } { const _j = {} as JsonTableColumn; + _o.set(_j, "coltype", _p?.coltype); _o.set(_j, "name", _p?.name); _o.set(_j, "typeName", _p?.typeName); @@ -1843,6 +2102,7 @@ export default { _o.set(_j, "on_empty", _p?.on_empty); _o.set(_j, "on_error", _p?.on_error); _o.set(_j, "location", _p?.location); + return { JsonTableColumn: _j }; @@ -1851,8 +2111,10 @@ export default { JsonKeyValue: JsonKeyValue; } { const _j = {} as JsonKeyValue; + _o.set(_j, "key", _p?.key); _o.set(_j, "value", _p?.value); + return { JsonKeyValue: _j }; @@ -1861,10 +2123,12 @@ export default { JsonParseExpr: JsonParseExpr; } { const _j = {} as JsonParseExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "unique_keys", _p?.unique_keys); _o.set(_j, "location", _p?.location); + return { JsonParseExpr: _j }; @@ -1873,9 +2137,11 @@ export default { JsonScalarExpr: JsonScalarExpr; } { const _j = {} as JsonScalarExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "location", _p?.location); + return { JsonScalarExpr: _j }; @@ -1884,9 +2150,11 @@ export default { JsonSerializeExpr: JsonSerializeExpr; } { const _j = {} as JsonSerializeExpr; + _o.set(_j, "expr", _p?.expr); _o.set(_j, "output", _p?.output); _o.set(_j, "location", _p?.location); + return { JsonSerializeExpr: _j }; @@ -1895,11 +2163,13 @@ export default { JsonObjectConstructor: JsonObjectConstructor; } { const _j = {} as JsonObjectConstructor; + _o.set(_j, "exprs", _p?.exprs); _o.set(_j, "output", _p?.output); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); _o.set(_j, "location", _p?.location); + return { JsonObjectConstructor: _j }; @@ -1908,10 +2178,12 @@ export default { JsonArrayConstructor: JsonArrayConstructor; } { const _j = {} as JsonArrayConstructor; + _o.set(_j, "exprs", _p?.exprs); _o.set(_j, "output", _p?.output); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "location", _p?.location); + return { JsonArrayConstructor: _j }; @@ -1920,11 +2192,13 @@ export default { JsonArrayQueryConstructor: JsonArrayQueryConstructor; } { const _j = {} as JsonArrayQueryConstructor; + _o.set(_j, "query", _p?.query); _o.set(_j, "output", _p?.output); _o.set(_j, "format", _p?.format); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "location", _p?.location); + return { JsonArrayQueryConstructor: _j }; @@ -1933,11 +2207,13 @@ export default { JsonAggConstructor: JsonAggConstructor; } { const _j = {} as JsonAggConstructor; + _o.set(_j, "output", _p?.output); _o.set(_j, "agg_filter", _p?.agg_filter); _o.set(_j, "agg_order", _p?.agg_order); _o.set(_j, "over", _p?.over); _o.set(_j, "location", _p?.location); + return { JsonAggConstructor: _j }; @@ -1946,10 +2222,12 @@ export default { JsonObjectAgg: JsonObjectAgg; } { const _j = {} as JsonObjectAgg; + _o.set(_j, "constructor", _p?.constructor); _o.set(_j, "arg", _p?.arg); _o.set(_j, "absent_on_null", _p?.absent_on_null); _o.set(_j, "unique", _p?.unique); + return { JsonObjectAgg: _j }; @@ -1958,9 +2236,11 @@ export default { JsonArrayAgg: JsonArrayAgg; } { const _j = {} as JsonArrayAgg; + _o.set(_j, "constructor", _p?.constructor); _o.set(_j, "arg", _p?.arg); _o.set(_j, "absent_on_null", _p?.absent_on_null); + return { JsonArrayAgg: _j }; @@ -1969,9 +2249,11 @@ export default { RawStmt: RawStmt; } { const _j = {} as RawStmt; + _o.set(_j, "stmt", _p?.stmt); _o.set(_j, "stmt_location", _p?.stmt_location); _o.set(_j, "stmt_len", _p?.stmt_len); + return { RawStmt: _j }; @@ -1980,6 +2262,7 @@ export default { InsertStmt: InsertStmt; } { const _j = {} as InsertStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "cols", _p?.cols); _o.set(_j, "selectStmt", _p?.selectStmt); @@ -1987,6 +2270,7 @@ export default { _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); _o.set(_j, "override", _p?.override); + return { InsertStmt: _j }; @@ -1995,11 +2279,13 @@ export default { DeleteStmt: DeleteStmt; } { const _j = {} as DeleteStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "usingClause", _p?.usingClause); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return { DeleteStmt: _j }; @@ -2008,12 +2294,14 @@ export default { UpdateStmt: UpdateStmt; } { const _j = {} as UpdateStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "targetList", _p?.targetList); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "fromClause", _p?.fromClause); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return { UpdateStmt: _j }; @@ -2022,12 +2310,14 @@ export default { MergeStmt: MergeStmt; } { const _j = {} as MergeStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "sourceRelation", _p?.sourceRelation); _o.set(_j, "joinCondition", _p?.joinCondition); _o.set(_j, "mergeWhenClauses", _p?.mergeWhenClauses); _o.set(_j, "returningList", _p?.returningList); _o.set(_j, "withClause", _p?.withClause); + return { MergeStmt: _j }; @@ -2036,6 +2326,7 @@ export default { SelectStmt: SelectStmt; } { const _j = {} as SelectStmt; + _o.set(_j, "distinctClause", _p?.distinctClause); _o.set(_j, "intoClause", _p?.intoClause); _o.set(_j, "targetList", _p?.targetList); @@ -2056,6 +2347,7 @@ export default { _o.set(_j, "all", _p?.all); _o.set(_j, "larg", _p?.larg); _o.set(_j, "rarg", _p?.rarg); + return { SelectStmt: _j }; @@ -2064,6 +2356,7 @@ export default { SetOperationStmt: SetOperationStmt; } { const _j = {} as SetOperationStmt; + _o.set(_j, "op", _p?.op); _o.set(_j, "all", _p?.all); _o.set(_j, "larg", _p?.larg); @@ -2072,6 +2365,7 @@ export default { _o.set(_j, "colTypmods", _p?.colTypmods); _o.set(_j, "colCollations", _p?.colCollations); _o.set(_j, "groupClauses", _p?.groupClauses); + return { SetOperationStmt: _j }; @@ -2080,7 +2374,9 @@ export default { ReturnStmt: ReturnStmt; } { const _j = {} as ReturnStmt; + _o.set(_j, "returnval", _p?.returnval); + return { ReturnStmt: _j }; @@ -2089,11 +2385,13 @@ export default { PLAssignStmt: PLAssignStmt; } { const _j = {} as PLAssignStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "indirection", _p?.indirection); _o.set(_j, "nnames", _p?.nnames); _o.set(_j, "val", _p?.val); _o.set(_j, "location", _p?.location); + return { PLAssignStmt: _j }; @@ -2102,10 +2400,12 @@ export default { CreateSchemaStmt: CreateSchemaStmt; } { const _j = {} as CreateSchemaStmt; + _o.set(_j, "schemaname", _p?.schemaname); _o.set(_j, "authrole", _p?.authrole); _o.set(_j, "schemaElts", _p?.schemaElts); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return { CreateSchemaStmt: _j }; @@ -2114,10 +2414,12 @@ export default { AlterTableStmt: AlterTableStmt; } { const _j = {} as AlterTableStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "cmds", _p?.cmds); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterTableStmt: _j }; @@ -2126,8 +2428,10 @@ export default { ReplicaIdentityStmt: ReplicaIdentityStmt; } { const _j = {} as ReplicaIdentityStmt; + _o.set(_j, "identity_type", _p?.identity_type); _o.set(_j, "name", _p?.name); + return { ReplicaIdentityStmt: _j }; @@ -2136,6 +2440,7 @@ export default { AlterTableCmd: AlterTableCmd; } { const _j = {} as AlterTableCmd; + _o.set(_j, "subtype", _p?.subtype); _o.set(_j, "name", _p?.name); _o.set(_j, "num", _p?.num); @@ -2144,6 +2449,7 @@ export default { _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "recurse", _p?.recurse); + return { AlterTableCmd: _j }; @@ -2152,7 +2458,9 @@ export default { AlterCollationStmt: AlterCollationStmt; } { const _j = {} as AlterCollationStmt; + _o.set(_j, "collname", _p?.collname); + return { AlterCollationStmt: _j }; @@ -2161,12 +2469,14 @@ export default { AlterDomainStmt: AlterDomainStmt; } { const _j = {} as AlterDomainStmt; + _o.set(_j, "subtype", _p?.subtype); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "name", _p?.name); _o.set(_j, "def", _p?.def); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterDomainStmt: _j }; @@ -2175,6 +2485,7 @@ export default { GrantStmt: GrantStmt; } { const _j = {} as GrantStmt; + _o.set(_j, "is_grant", _p?.is_grant); _o.set(_j, "targtype", _p?.targtype); _o.set(_j, "objtype", _p?.objtype); @@ -2184,6 +2495,7 @@ export default { _o.set(_j, "grant_option", _p?.grant_option); _o.set(_j, "grantor", _p?.grantor); _o.set(_j, "behavior", _p?.behavior); + return { GrantStmt: _j }; @@ -2192,10 +2504,12 @@ export default { ObjectWithArgs: ObjectWithArgs; } { const _j = {} as ObjectWithArgs; + _o.set(_j, "objname", _p?.objname); _o.set(_j, "objargs", _p?.objargs); _o.set(_j, "objfuncargs", _p?.objfuncargs); _o.set(_j, "args_unspecified", _p?.args_unspecified); + return { ObjectWithArgs: _j }; @@ -2204,8 +2518,10 @@ export default { AccessPriv: AccessPriv; } { const _j = {} as AccessPriv; + _o.set(_j, "priv_name", _p?.priv_name); _o.set(_j, "cols", _p?.cols); + return { AccessPriv: _j }; @@ -2214,12 +2530,14 @@ export default { GrantRoleStmt: GrantRoleStmt; } { const _j = {} as GrantRoleStmt; + _o.set(_j, "granted_roles", _p?.granted_roles); _o.set(_j, "grantee_roles", _p?.grantee_roles); _o.set(_j, "is_grant", _p?.is_grant); _o.set(_j, "opt", _p?.opt); _o.set(_j, "grantor", _p?.grantor); _o.set(_j, "behavior", _p?.behavior); + return { GrantRoleStmt: _j }; @@ -2228,8 +2546,10 @@ export default { AlterDefaultPrivilegesStmt: AlterDefaultPrivilegesStmt; } { const _j = {} as AlterDefaultPrivilegesStmt; + _o.set(_j, "options", _p?.options); _o.set(_j, "action", _p?.action); + return { AlterDefaultPrivilegesStmt: _j }; @@ -2238,6 +2558,7 @@ export default { CopyStmt: CopyStmt; } { const _j = {} as CopyStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "query", _p?.query); _o.set(_j, "attlist", _p?.attlist); @@ -2246,6 +2567,7 @@ export default { _o.set(_j, "filename", _p?.filename); _o.set(_j, "options", _p?.options); _o.set(_j, "whereClause", _p?.whereClause); + return { CopyStmt: _j }; @@ -2254,10 +2576,12 @@ export default { VariableSetStmt: VariableSetStmt; } { const _j = {} as VariableSetStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "name", _p?.name); _o.set(_j, "args", _p?.args); _o.set(_j, "is_local", _p?.is_local); + return { VariableSetStmt: _j }; @@ -2266,7 +2590,9 @@ export default { VariableShowStmt: VariableShowStmt; } { const _j = {} as VariableShowStmt; + _o.set(_j, "name", _p?.name); + return { VariableShowStmt: _j }; @@ -2275,6 +2601,7 @@ export default { CreateStmt: CreateStmt; } { const _j = {} as CreateStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "tableElts", _p?.tableElts); _o.set(_j, "inhRelations", _p?.inhRelations); @@ -2287,6 +2614,7 @@ export default { _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "accessMethod", _p?.accessMethod); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return { CreateStmt: _j }; @@ -2295,6 +2623,7 @@ export default { Constraint: Constraint; } { const _j = {} as Constraint; + _o.set(_j, "contype", _p?.contype); _o.set(_j, "conname", _p?.conname); _o.set(_j, "deferrable", _p?.deferrable); @@ -2326,6 +2655,7 @@ export default { _o.set(_j, "old_conpfeqop", _p?.old_conpfeqop); _o.set(_j, "old_pktable_oid", _p?.old_pktable_oid); _o.set(_j, "location", _p?.location); + return { Constraint: _j }; @@ -2334,10 +2664,12 @@ export default { CreateTableSpaceStmt: CreateTableSpaceStmt; } { const _j = {} as CreateTableSpaceStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "owner", _p?.owner); _o.set(_j, "location", _p?.location); _o.set(_j, "options", _p?.options); + return { CreateTableSpaceStmt: _j }; @@ -2346,8 +2678,10 @@ export default { DropTableSpaceStmt: DropTableSpaceStmt; } { const _j = {} as DropTableSpaceStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "missing_ok", _p?.missing_ok); + return { DropTableSpaceStmt: _j }; @@ -2356,9 +2690,11 @@ export default { AlterTableSpaceOptionsStmt: AlterTableSpaceOptionsStmt; } { const _j = {} as AlterTableSpaceOptionsStmt; + _o.set(_j, "tablespacename", _p?.tablespacename); _o.set(_j, "options", _p?.options); _o.set(_j, "isReset", _p?.isReset); + return { AlterTableSpaceOptionsStmt: _j }; @@ -2367,11 +2703,13 @@ export default { AlterTableMoveAllStmt: AlterTableMoveAllStmt; } { const _j = {} as AlterTableMoveAllStmt; + _o.set(_j, "orig_tablespacename", _p?.orig_tablespacename); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "roles", _p?.roles); _o.set(_j, "new_tablespacename", _p?.new_tablespacename); _o.set(_j, "nowait", _p?.nowait); + return { AlterTableMoveAllStmt: _j }; @@ -2380,9 +2718,11 @@ export default { CreateExtensionStmt: CreateExtensionStmt; } { const _j = {} as CreateExtensionStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return { CreateExtensionStmt: _j }; @@ -2391,8 +2731,10 @@ export default { AlterExtensionStmt: AlterExtensionStmt; } { const _j = {} as AlterExtensionStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "options", _p?.options); + return { AlterExtensionStmt: _j }; @@ -2401,10 +2743,12 @@ export default { AlterExtensionContentsStmt: AlterExtensionContentsStmt; } { const _j = {} as AlterExtensionContentsStmt; + _o.set(_j, "extname", _p?.extname); _o.set(_j, "action", _p?.action); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); + return { AlterExtensionContentsStmt: _j }; @@ -2413,9 +2757,11 @@ export default { CreateFdwStmt: CreateFdwStmt; } { const _j = {} as CreateFdwStmt; + _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "func_options", _p?.func_options); _o.set(_j, "options", _p?.options); + return { CreateFdwStmt: _j }; @@ -2424,9 +2770,11 @@ export default { AlterFdwStmt: AlterFdwStmt; } { const _j = {} as AlterFdwStmt; + _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "func_options", _p?.func_options); _o.set(_j, "options", _p?.options); + return { AlterFdwStmt: _j }; @@ -2435,12 +2783,14 @@ export default { CreateForeignServerStmt: CreateForeignServerStmt; } { const _j = {} as CreateForeignServerStmt; + _o.set(_j, "servername", _p?.servername); _o.set(_j, "servertype", _p?.servertype); _o.set(_j, "version", _p?.version); _o.set(_j, "fdwname", _p?.fdwname); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return { CreateForeignServerStmt: _j }; @@ -2449,10 +2799,12 @@ export default { AlterForeignServerStmt: AlterForeignServerStmt; } { const _j = {} as AlterForeignServerStmt; + _o.set(_j, "servername", _p?.servername); _o.set(_j, "version", _p?.version); _o.set(_j, "options", _p?.options); _o.set(_j, "has_version", _p?.has_version); + return { AlterForeignServerStmt: _j }; @@ -2461,9 +2813,11 @@ export default { CreateForeignTableStmt: CreateForeignTableStmt; } { const _j = {} as CreateForeignTableStmt; + _o.set(_j, "base", _p?.base); _o.set(_j, "servername", _p?.servername); _o.set(_j, "options", _p?.options); + return { CreateForeignTableStmt: _j }; @@ -2472,10 +2826,12 @@ export default { CreateUserMappingStmt: CreateUserMappingStmt; } { const _j = {} as CreateUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "options", _p?.options); + return { CreateUserMappingStmt: _j }; @@ -2484,9 +2840,11 @@ export default { AlterUserMappingStmt: AlterUserMappingStmt; } { const _j = {} as AlterUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "options", _p?.options); + return { AlterUserMappingStmt: _j }; @@ -2495,9 +2853,11 @@ export default { DropUserMappingStmt: DropUserMappingStmt; } { const _j = {} as DropUserMappingStmt; + _o.set(_j, "user", _p?.user); _o.set(_j, "servername", _p?.servername); _o.set(_j, "missing_ok", _p?.missing_ok); + return { DropUserMappingStmt: _j }; @@ -2506,12 +2866,14 @@ export default { ImportForeignSchemaStmt: ImportForeignSchemaStmt; } { const _j = {} as ImportForeignSchemaStmt; + _o.set(_j, "server_name", _p?.server_name); _o.set(_j, "remote_schema", _p?.remote_schema); _o.set(_j, "local_schema", _p?.local_schema); _o.set(_j, "list_type", _p?.list_type); _o.set(_j, "table_list", _p?.table_list); _o.set(_j, "options", _p?.options); + return { ImportForeignSchemaStmt: _j }; @@ -2520,6 +2882,7 @@ export default { CreatePolicyStmt: CreatePolicyStmt; } { const _j = {} as CreatePolicyStmt; + _o.set(_j, "policy_name", _p?.policy_name); _o.set(_j, "table", _p?.table); _o.set(_j, "cmd_name", _p?.cmd_name); @@ -2527,6 +2890,7 @@ export default { _o.set(_j, "roles", _p?.roles); _o.set(_j, "qual", _p?.qual); _o.set(_j, "with_check", _p?.with_check); + return { CreatePolicyStmt: _j }; @@ -2535,11 +2899,13 @@ export default { AlterPolicyStmt: AlterPolicyStmt; } { const _j = {} as AlterPolicyStmt; + _o.set(_j, "policy_name", _p?.policy_name); _o.set(_j, "table", _p?.table); _o.set(_j, "roles", _p?.roles); _o.set(_j, "qual", _p?.qual); _o.set(_j, "with_check", _p?.with_check); + return { AlterPolicyStmt: _j }; @@ -2548,9 +2914,11 @@ export default { CreateAmStmt: CreateAmStmt; } { const _j = {} as CreateAmStmt; + _o.set(_j, "amname", _p?.amname); _o.set(_j, "handler_name", _p?.handler_name); _o.set(_j, "amtype", _p?.amtype); + return { CreateAmStmt: _j }; @@ -2559,6 +2927,7 @@ export default { CreateTrigStmt: CreateTrigStmt; } { const _j = {} as CreateTrigStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "isconstraint", _p?.isconstraint); _o.set(_j, "trigname", _p?.trigname); @@ -2574,6 +2943,7 @@ export default { _o.set(_j, "deferrable", _p?.deferrable); _o.set(_j, "initdeferred", _p?.initdeferred); _o.set(_j, "constrrel", _p?.constrrel); + return { CreateTrigStmt: _j }; @@ -2582,10 +2952,12 @@ export default { CreateEventTrigStmt: CreateEventTrigStmt; } { const _j = {} as CreateEventTrigStmt; + _o.set(_j, "trigname", _p?.trigname); _o.set(_j, "eventname", _p?.eventname); _o.set(_j, "whenclause", _p?.whenclause); _o.set(_j, "funcname", _p?.funcname); + return { CreateEventTrigStmt: _j }; @@ -2594,8 +2966,10 @@ export default { AlterEventTrigStmt: AlterEventTrigStmt; } { const _j = {} as AlterEventTrigStmt; + _o.set(_j, "trigname", _p?.trigname); _o.set(_j, "tgenabled", _p?.tgenabled); + return { AlterEventTrigStmt: _j }; @@ -2604,12 +2978,14 @@ export default { CreatePLangStmt: CreatePLangStmt; } { const _j = {} as CreatePLangStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "plname", _p?.plname); _o.set(_j, "plhandler", _p?.plhandler); _o.set(_j, "plinline", _p?.plinline); _o.set(_j, "plvalidator", _p?.plvalidator); _o.set(_j, "pltrusted", _p?.pltrusted); + return { CreatePLangStmt: _j }; @@ -2618,9 +2994,11 @@ export default { CreateRoleStmt: CreateRoleStmt; } { const _j = {} as CreateRoleStmt; + _o.set(_j, "stmt_type", _p?.stmt_type); _o.set(_j, "role", _p?.role); _o.set(_j, "options", _p?.options); + return { CreateRoleStmt: _j }; @@ -2629,9 +3007,11 @@ export default { AlterRoleStmt: AlterRoleStmt; } { const _j = {} as AlterRoleStmt; + _o.set(_j, "role", _p?.role); _o.set(_j, "options", _p?.options); _o.set(_j, "action", _p?.action); + return { AlterRoleStmt: _j }; @@ -2640,9 +3020,11 @@ export default { AlterRoleSetStmt: AlterRoleSetStmt; } { const _j = {} as AlterRoleSetStmt; + _o.set(_j, "role", _p?.role); _o.set(_j, "database", _p?.database); _o.set(_j, "setstmt", _p?.setstmt); + return { AlterRoleSetStmt: _j }; @@ -2651,8 +3033,10 @@ export default { DropRoleStmt: DropRoleStmt; } { const _j = {} as DropRoleStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "missing_ok", _p?.missing_ok); + return { DropRoleStmt: _j }; @@ -2661,11 +3045,13 @@ export default { CreateSeqStmt: CreateSeqStmt; } { const _j = {} as CreateSeqStmt; + _o.set(_j, "sequence", _p?.sequence); _o.set(_j, "options", _p?.options); _o.set(_j, "ownerId", _p?.ownerId); _o.set(_j, "for_identity", _p?.for_identity); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return { CreateSeqStmt: _j }; @@ -2674,10 +3060,12 @@ export default { AlterSeqStmt: AlterSeqStmt; } { const _j = {} as AlterSeqStmt; + _o.set(_j, "sequence", _p?.sequence); _o.set(_j, "options", _p?.options); _o.set(_j, "for_identity", _p?.for_identity); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterSeqStmt: _j }; @@ -2686,6 +3074,7 @@ export default { DefineStmt: DefineStmt; } { const _j = {} as DefineStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "oldstyle", _p?.oldstyle); _o.set(_j, "defnames", _p?.defnames); @@ -2693,6 +3082,7 @@ export default { _o.set(_j, "definition", _p?.definition); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "replace", _p?.replace); + return { DefineStmt: _j }; @@ -2701,10 +3091,12 @@ export default { CreateDomainStmt: CreateDomainStmt; } { const _j = {} as CreateDomainStmt; + _o.set(_j, "domainname", _p?.domainname); _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "collClause", _p?.collClause); _o.set(_j, "constraints", _p?.constraints); + return { CreateDomainStmt: _j }; @@ -2713,12 +3105,14 @@ export default { CreateOpClassStmt: CreateOpClassStmt; } { const _j = {} as CreateOpClassStmt; + _o.set(_j, "opclassname", _p?.opclassname); _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); _o.set(_j, "datatype", _p?.datatype); _o.set(_j, "items", _p?.items); _o.set(_j, "isDefault", _p?.isDefault); + return { CreateOpClassStmt: _j }; @@ -2727,12 +3121,14 @@ export default { CreateOpClassItem: CreateOpClassItem; } { const _j = {} as CreateOpClassItem; + _o.set(_j, "itemtype", _p?.itemtype); _o.set(_j, "name", _p?.name); _o.set(_j, "number", _p?.number); _o.set(_j, "order_family", _p?.order_family); _o.set(_j, "class_args", _p?.class_args); _o.set(_j, "storedtype", _p?.storedtype); + return { CreateOpClassItem: _j }; @@ -2741,8 +3137,10 @@ export default { CreateOpFamilyStmt: CreateOpFamilyStmt; } { const _j = {} as CreateOpFamilyStmt; + _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); + return { CreateOpFamilyStmt: _j }; @@ -2751,10 +3149,12 @@ export default { AlterOpFamilyStmt: AlterOpFamilyStmt; } { const _j = {} as AlterOpFamilyStmt; + _o.set(_j, "opfamilyname", _p?.opfamilyname); _o.set(_j, "amname", _p?.amname); _o.set(_j, "isDrop", _p?.isDrop); _o.set(_j, "items", _p?.items); + return { AlterOpFamilyStmt: _j }; @@ -2763,11 +3163,13 @@ export default { DropStmt: DropStmt; } { const _j = {} as DropStmt; + _o.set(_j, "objects", _p?.objects); _o.set(_j, "removeType", _p?.removeType); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "concurrent", _p?.concurrent); + return { DropStmt: _j }; @@ -2776,9 +3178,11 @@ export default { TruncateStmt: TruncateStmt; } { const _j = {} as TruncateStmt; + _o.set(_j, "relations", _p?.relations); _o.set(_j, "restart_seqs", _p?.restart_seqs); _o.set(_j, "behavior", _p?.behavior); + return { TruncateStmt: _j }; @@ -2787,9 +3191,11 @@ export default { CommentStmt: CommentStmt; } { const _j = {} as CommentStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); _o.set(_j, "comment", _p?.comment); + return { CommentStmt: _j }; @@ -2798,10 +3204,12 @@ export default { SecLabelStmt: SecLabelStmt; } { const _j = {} as SecLabelStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "object", _p?.object); _o.set(_j, "provider", _p?.provider); _o.set(_j, "label", _p?.label); + return { SecLabelStmt: _j }; @@ -2810,9 +3218,11 @@ export default { DeclareCursorStmt: DeclareCursorStmt; } { const _j = {} as DeclareCursorStmt; + _o.set(_j, "portalname", _p?.portalname); _o.set(_j, "options", _p?.options); _o.set(_j, "query", _p?.query); + return { DeclareCursorStmt: _j }; @@ -2821,7 +3231,9 @@ export default { ClosePortalStmt: ClosePortalStmt; } { const _j = {} as ClosePortalStmt; + _o.set(_j, "portalname", _p?.portalname); + return { ClosePortalStmt: _j }; @@ -2830,10 +3242,12 @@ export default { FetchStmt: FetchStmt; } { const _j = {} as FetchStmt; + _o.set(_j, "direction", _p?.direction); _o.set(_j, "howMany", _p?.howMany); _o.set(_j, "portalname", _p?.portalname); _o.set(_j, "ismove", _p?.ismove); + return { FetchStmt: _j }; @@ -2842,6 +3256,7 @@ export default { IndexStmt: IndexStmt; } { const _j = {} as IndexStmt; + _o.set(_j, "idxname", _p?.idxname); _o.set(_j, "relation", _p?.relation); _o.set(_j, "accessMethod", _p?.accessMethod); @@ -2866,6 +3281,7 @@ export default { _o.set(_j, "concurrent", _p?.concurrent); _o.set(_j, "if_not_exists", _p?.if_not_exists); _o.set(_j, "reset_default_tblspc", _p?.reset_default_tblspc); + return { IndexStmt: _j }; @@ -2874,6 +3290,7 @@ export default { CreateStatsStmt: CreateStatsStmt; } { const _j = {} as CreateStatsStmt; + _o.set(_j, "defnames", _p?.defnames); _o.set(_j, "stat_types", _p?.stat_types); _o.set(_j, "exprs", _p?.exprs); @@ -2881,6 +3298,7 @@ export default { _o.set(_j, "stxcomment", _p?.stxcomment); _o.set(_j, "transformed", _p?.transformed); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return { CreateStatsStmt: _j }; @@ -2889,8 +3307,10 @@ export default { StatsElem: StatsElem; } { const _j = {} as StatsElem; + _o.set(_j, "name", _p?.name); _o.set(_j, "expr", _p?.expr); + return { StatsElem: _j }; @@ -2899,9 +3319,11 @@ export default { AlterStatsStmt: AlterStatsStmt; } { const _j = {} as AlterStatsStmt; + _o.set(_j, "defnames", _p?.defnames); _o.set(_j, "stxstattarget", _p?.stxstattarget); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterStatsStmt: _j }; @@ -2910,6 +3332,7 @@ export default { CreateFunctionStmt: CreateFunctionStmt; } { const _j = {} as CreateFunctionStmt; + _o.set(_j, "is_procedure", _p?.is_procedure); _o.set(_j, "replace", _p?.replace); _o.set(_j, "funcname", _p?.funcname); @@ -2917,6 +3340,7 @@ export default { _o.set(_j, "returnType", _p?.returnType); _o.set(_j, "options", _p?.options); _o.set(_j, "sql_body", _p?.sql_body); + return { CreateFunctionStmt: _j }; @@ -2925,10 +3349,12 @@ export default { FunctionParameter: FunctionParameter; } { const _j = {} as FunctionParameter; + _o.set(_j, "name", _p?.name); _o.set(_j, "argType", _p?.argType); _o.set(_j, "mode", _p?.mode); _o.set(_j, "defexpr", _p?.defexpr); + return { FunctionParameter: _j }; @@ -2937,9 +3363,11 @@ export default { AlterFunctionStmt: AlterFunctionStmt; } { const _j = {} as AlterFunctionStmt; + _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "func", _p?.func); _o.set(_j, "actions", _p?.actions); + return { AlterFunctionStmt: _j }; @@ -2948,7 +3376,9 @@ export default { DoStmt: DoStmt; } { const _j = {} as DoStmt; + _o.set(_j, "args", _p?.args); + return { DoStmt: _j }; @@ -2957,10 +3387,12 @@ export default { InlineCodeBlock: InlineCodeBlock; } { const _j = {} as InlineCodeBlock; + _o.set(_j, "source_text", _p?.source_text); _o.set(_j, "langOid", _p?.langOid); _o.set(_j, "langIsTrusted", _p?.langIsTrusted); _o.set(_j, "atomic", _p?.atomic); + return { InlineCodeBlock: _j }; @@ -2969,9 +3401,11 @@ export default { CallStmt: CallStmt; } { const _j = {} as CallStmt; + _o.set(_j, "funccall", _p?.funccall); _o.set(_j, "funcexpr", _p?.funcexpr); _o.set(_j, "outargs", _p?.outargs); + return { CallStmt: _j }; @@ -2980,7 +3414,9 @@ export default { CallContext: CallContext; } { const _j = {} as CallContext; + _o.set(_j, "atomic", _p?.atomic); + return { CallContext: _j }; @@ -2989,6 +3425,7 @@ export default { RenameStmt: RenameStmt; } { const _j = {} as RenameStmt; + _o.set(_j, "renameType", _p?.renameType); _o.set(_j, "relationType", _p?.relationType); _o.set(_j, "relation", _p?.relation); @@ -2997,6 +3434,7 @@ export default { _o.set(_j, "newname", _p?.newname); _o.set(_j, "behavior", _p?.behavior); _o.set(_j, "missing_ok", _p?.missing_ok); + return { RenameStmt: _j }; @@ -3005,11 +3443,13 @@ export default { AlterObjectDependsStmt: AlterObjectDependsStmt; } { const _j = {} as AlterObjectDependsStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "extname", _p?.extname); _o.set(_j, "remove", _p?.remove); + return { AlterObjectDependsStmt: _j }; @@ -3018,11 +3458,13 @@ export default { AlterObjectSchemaStmt: AlterObjectSchemaStmt; } { const _j = {} as AlterObjectSchemaStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "newschema", _p?.newschema); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterObjectSchemaStmt: _j }; @@ -3031,10 +3473,12 @@ export default { AlterOwnerStmt: AlterOwnerStmt; } { const _j = {} as AlterOwnerStmt; + _o.set(_j, "objectType", _p?.objectType); _o.set(_j, "relation", _p?.relation); _o.set(_j, "object", _p?.object); _o.set(_j, "newowner", _p?.newowner); + return { AlterOwnerStmt: _j }; @@ -3043,8 +3487,10 @@ export default { AlterOperatorStmt: AlterOperatorStmt; } { const _j = {} as AlterOperatorStmt; + _o.set(_j, "opername", _p?.opername); _o.set(_j, "options", _p?.options); + return { AlterOperatorStmt: _j }; @@ -3053,8 +3499,10 @@ export default { AlterTypeStmt: AlterTypeStmt; } { const _j = {} as AlterTypeStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "options", _p?.options); + return { AlterTypeStmt: _j }; @@ -3063,6 +3511,7 @@ export default { RuleStmt: RuleStmt; } { const _j = {} as RuleStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "rulename", _p?.rulename); _o.set(_j, "whereClause", _p?.whereClause); @@ -3070,6 +3519,7 @@ export default { _o.set(_j, "instead", _p?.instead); _o.set(_j, "actions", _p?.actions); _o.set(_j, "replace", _p?.replace); + return { RuleStmt: _j }; @@ -3078,8 +3528,10 @@ export default { NotifyStmt: NotifyStmt; } { const _j = {} as NotifyStmt; + _o.set(_j, "conditionname", _p?.conditionname); _o.set(_j, "payload", _p?.payload); + return { NotifyStmt: _j }; @@ -3088,7 +3540,9 @@ export default { ListenStmt: ListenStmt; } { const _j = {} as ListenStmt; + _o.set(_j, "conditionname", _p?.conditionname); + return { ListenStmt: _j }; @@ -3097,7 +3551,9 @@ export default { UnlistenStmt: UnlistenStmt; } { const _j = {} as UnlistenStmt; + _o.set(_j, "conditionname", _p?.conditionname); + return { UnlistenStmt: _j }; @@ -3106,12 +3562,14 @@ export default { TransactionStmt: TransactionStmt; } { const _j = {} as TransactionStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "options", _p?.options); _o.set(_j, "savepoint_name", _p?.savepoint_name); _o.set(_j, "gid", _p?.gid); _o.set(_j, "chain", _p?.chain); _o.set(_j, "location", _p?.location); + return { TransactionStmt: _j }; @@ -3120,8 +3578,10 @@ export default { CompositeTypeStmt: CompositeTypeStmt; } { const _j = {} as CompositeTypeStmt; + _o.set(_j, "typevar", _p?.typevar); _o.set(_j, "coldeflist", _p?.coldeflist); + return { CompositeTypeStmt: _j }; @@ -3130,8 +3590,10 @@ export default { CreateEnumStmt: CreateEnumStmt; } { const _j = {} as CreateEnumStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "vals", _p?.vals); + return { CreateEnumStmt: _j }; @@ -3140,8 +3602,10 @@ export default { CreateRangeStmt: CreateRangeStmt; } { const _j = {} as CreateRangeStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "params", _p?.params); + return { CreateRangeStmt: _j }; @@ -3150,12 +3614,14 @@ export default { AlterEnumStmt: AlterEnumStmt; } { const _j = {} as AlterEnumStmt; + _o.set(_j, "typeName", _p?.typeName); _o.set(_j, "oldVal", _p?.oldVal); _o.set(_j, "newVal", _p?.newVal); _o.set(_j, "newValNeighbor", _p?.newValNeighbor); _o.set(_j, "newValIsAfter", _p?.newValIsAfter); _o.set(_j, "skipIfNewValExists", _p?.skipIfNewValExists); + return { AlterEnumStmt: _j }; @@ -3164,12 +3630,14 @@ export default { ViewStmt: ViewStmt; } { const _j = {} as ViewStmt; + _o.set(_j, "view", _p?.view); _o.set(_j, "aliases", _p?.aliases); _o.set(_j, "query", _p?.query); _o.set(_j, "replace", _p?.replace); _o.set(_j, "options", _p?.options); _o.set(_j, "withCheckOption", _p?.withCheckOption); + return { ViewStmt: _j }; @@ -3178,7 +3646,9 @@ export default { LoadStmt: LoadStmt; } { const _j = {} as LoadStmt; + _o.set(_j, "filename", _p?.filename); + return { LoadStmt: _j }; @@ -3187,8 +3657,10 @@ export default { CreatedbStmt: CreatedbStmt; } { const _j = {} as CreatedbStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "options", _p?.options); + return { CreatedbStmt: _j }; @@ -3197,8 +3669,10 @@ export default { AlterDatabaseStmt: AlterDatabaseStmt; } { const _j = {} as AlterDatabaseStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "options", _p?.options); + return { AlterDatabaseStmt: _j }; @@ -3207,7 +3681,9 @@ export default { AlterDatabaseRefreshCollStmt: AlterDatabaseRefreshCollStmt; } { const _j = {} as AlterDatabaseRefreshCollStmt; + _o.set(_j, "dbname", _p?.dbname); + return { AlterDatabaseRefreshCollStmt: _j }; @@ -3216,8 +3692,10 @@ export default { AlterDatabaseSetStmt: AlterDatabaseSetStmt; } { const _j = {} as AlterDatabaseSetStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "setstmt", _p?.setstmt); + return { AlterDatabaseSetStmt: _j }; @@ -3226,9 +3704,11 @@ export default { DropdbStmt: DropdbStmt; } { const _j = {} as DropdbStmt; + _o.set(_j, "dbname", _p?.dbname); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "options", _p?.options); + return { DropdbStmt: _j }; @@ -3237,7 +3717,9 @@ export default { AlterSystemStmt: AlterSystemStmt; } { const _j = {} as AlterSystemStmt; + _o.set(_j, "setstmt", _p?.setstmt); + return { AlterSystemStmt: _j }; @@ -3246,9 +3728,11 @@ export default { ClusterStmt: ClusterStmt; } { const _j = {} as ClusterStmt; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "indexname", _p?.indexname); _o.set(_j, "params", _p?.params); + return { ClusterStmt: _j }; @@ -3257,9 +3741,11 @@ export default { VacuumStmt: VacuumStmt; } { const _j = {} as VacuumStmt; + _o.set(_j, "options", _p?.options); _o.set(_j, "rels", _p?.rels); _o.set(_j, "is_vacuumcmd", _p?.is_vacuumcmd); + return { VacuumStmt: _j }; @@ -3268,9 +3754,11 @@ export default { VacuumRelation: VacuumRelation; } { const _j = {} as VacuumRelation; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "oid", _p?.oid); _o.set(_j, "va_cols", _p?.va_cols); + return { VacuumRelation: _j }; @@ -3279,8 +3767,10 @@ export default { ExplainStmt: ExplainStmt; } { const _j = {} as ExplainStmt; + _o.set(_j, "query", _p?.query); _o.set(_j, "options", _p?.options); + return { ExplainStmt: _j }; @@ -3289,11 +3779,13 @@ export default { CreateTableAsStmt: CreateTableAsStmt; } { const _j = {} as CreateTableAsStmt; + _o.set(_j, "query", _p?.query); _o.set(_j, "into", _p?.into); _o.set(_j, "objtype", _p?.objtype); _o.set(_j, "is_select_into", _p?.is_select_into); _o.set(_j, "if_not_exists", _p?.if_not_exists); + return { CreateTableAsStmt: _j }; @@ -3302,9 +3794,11 @@ export default { RefreshMatViewStmt: RefreshMatViewStmt; } { const _j = {} as RefreshMatViewStmt; + _o.set(_j, "concurrent", _p?.concurrent); _o.set(_j, "skipData", _p?.skipData); _o.set(_j, "relation", _p?.relation); + return { RefreshMatViewStmt: _j }; @@ -3313,6 +3807,7 @@ export default { CheckPointStmt: CheckPointStmt; } { const _j = {} as CheckPointStmt; + return { CheckPointStmt: _j }; @@ -3321,7 +3816,9 @@ export default { DiscardStmt: DiscardStmt; } { const _j = {} as DiscardStmt; + _o.set(_j, "target", _p?.target); + return { DiscardStmt: _j }; @@ -3330,9 +3827,11 @@ export default { LockStmt: LockStmt; } { const _j = {} as LockStmt; + _o.set(_j, "relations", _p?.relations); _o.set(_j, "mode", _p?.mode); _o.set(_j, "nowait", _p?.nowait); + return { LockStmt: _j }; @@ -3341,8 +3840,10 @@ export default { ConstraintsSetStmt: ConstraintsSetStmt; } { const _j = {} as ConstraintsSetStmt; + _o.set(_j, "constraints", _p?.constraints); _o.set(_j, "deferred", _p?.deferred); + return { ConstraintsSetStmt: _j }; @@ -3351,10 +3852,12 @@ export default { ReindexStmt: ReindexStmt; } { const _j = {} as ReindexStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "relation", _p?.relation); _o.set(_j, "name", _p?.name); _o.set(_j, "params", _p?.params); + return { ReindexStmt: _j }; @@ -3363,11 +3866,13 @@ export default { CreateConversionStmt: CreateConversionStmt; } { const _j = {} as CreateConversionStmt; + _o.set(_j, "conversion_name", _p?.conversion_name); _o.set(_j, "for_encoding_name", _p?.for_encoding_name); _o.set(_j, "to_encoding_name", _p?.to_encoding_name); _o.set(_j, "func_name", _p?.func_name); _o.set(_j, "def", _p?.def); + return { CreateConversionStmt: _j }; @@ -3376,11 +3881,13 @@ export default { CreateCastStmt: CreateCastStmt; } { const _j = {} as CreateCastStmt; + _o.set(_j, "sourcetype", _p?.sourcetype); _o.set(_j, "targettype", _p?.targettype); _o.set(_j, "func", _p?.func); _o.set(_j, "context", _p?.context); _o.set(_j, "inout", _p?.inout); + return { CreateCastStmt: _j }; @@ -3389,11 +3896,13 @@ export default { CreateTransformStmt: CreateTransformStmt; } { const _j = {} as CreateTransformStmt; + _o.set(_j, "replace", _p?.replace); _o.set(_j, "type_name", _p?.type_name); _o.set(_j, "lang", _p?.lang); _o.set(_j, "fromsql", _p?.fromsql); _o.set(_j, "tosql", _p?.tosql); + return { CreateTransformStmt: _j }; @@ -3402,9 +3911,11 @@ export default { PrepareStmt: PrepareStmt; } { const _j = {} as PrepareStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "argtypes", _p?.argtypes); _o.set(_j, "query", _p?.query); + return { PrepareStmt: _j }; @@ -3413,8 +3924,10 @@ export default { ExecuteStmt: ExecuteStmt; } { const _j = {} as ExecuteStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "params", _p?.params); + return { ExecuteStmt: _j }; @@ -3423,9 +3936,11 @@ export default { DeallocateStmt: DeallocateStmt; } { const _j = {} as DeallocateStmt; + _o.set(_j, "name", _p?.name); _o.set(_j, "isall", _p?.isall); _o.set(_j, "location", _p?.location); + return { DeallocateStmt: _j }; @@ -3434,8 +3949,10 @@ export default { DropOwnedStmt: DropOwnedStmt; } { const _j = {} as DropOwnedStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "behavior", _p?.behavior); + return { DropOwnedStmt: _j }; @@ -3444,8 +3961,10 @@ export default { ReassignOwnedStmt: ReassignOwnedStmt; } { const _j = {} as ReassignOwnedStmt; + _o.set(_j, "roles", _p?.roles); _o.set(_j, "newrole", _p?.newrole); + return { ReassignOwnedStmt: _j }; @@ -3454,8 +3973,10 @@ export default { AlterTSDictionaryStmt: AlterTSDictionaryStmt; } { const _j = {} as AlterTSDictionaryStmt; + _o.set(_j, "dictname", _p?.dictname); _o.set(_j, "options", _p?.options); + return { AlterTSDictionaryStmt: _j }; @@ -3464,6 +3985,7 @@ export default { AlterTSConfigurationStmt: AlterTSConfigurationStmt; } { const _j = {} as AlterTSConfigurationStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "cfgname", _p?.cfgname); _o.set(_j, "tokentype", _p?.tokentype); @@ -3471,6 +3993,7 @@ export default { _o.set(_j, "override", _p?.override); _o.set(_j, "replace", _p?.replace); _o.set(_j, "missing_ok", _p?.missing_ok); + return { AlterTSConfigurationStmt: _j }; @@ -3479,9 +4002,11 @@ export default { PublicationTable: PublicationTable; } { const _j = {} as PublicationTable; + _o.set(_j, "relation", _p?.relation); _o.set(_j, "whereClause", _p?.whereClause); _o.set(_j, "columns", _p?.columns); + return { PublicationTable: _j }; @@ -3490,10 +4015,12 @@ export default { PublicationObjSpec: PublicationObjSpec; } { const _j = {} as PublicationObjSpec; + _o.set(_j, "pubobjtype", _p?.pubobjtype); _o.set(_j, "name", _p?.name); _o.set(_j, "pubtable", _p?.pubtable); _o.set(_j, "location", _p?.location); + return { PublicationObjSpec: _j }; @@ -3502,10 +4029,12 @@ export default { CreatePublicationStmt: CreatePublicationStmt; } { const _j = {} as CreatePublicationStmt; + _o.set(_j, "pubname", _p?.pubname); _o.set(_j, "options", _p?.options); _o.set(_j, "pubobjects", _p?.pubobjects); _o.set(_j, "for_all_tables", _p?.for_all_tables); + return { CreatePublicationStmt: _j }; @@ -3514,11 +4043,13 @@ export default { AlterPublicationStmt: AlterPublicationStmt; } { const _j = {} as AlterPublicationStmt; + _o.set(_j, "pubname", _p?.pubname); _o.set(_j, "options", _p?.options); _o.set(_j, "pubobjects", _p?.pubobjects); _o.set(_j, "for_all_tables", _p?.for_all_tables); _o.set(_j, "action", _p?.action); + return { AlterPublicationStmt: _j }; @@ -3527,10 +4058,12 @@ export default { CreateSubscriptionStmt: CreateSubscriptionStmt; } { const _j = {} as CreateSubscriptionStmt; + _o.set(_j, "subname", _p?.subname); _o.set(_j, "conninfo", _p?.conninfo); _o.set(_j, "publication", _p?.publication); _o.set(_j, "options", _p?.options); + return { CreateSubscriptionStmt: _j }; @@ -3539,11 +4072,13 @@ export default { AlterSubscriptionStmt: AlterSubscriptionStmt; } { const _j = {} as AlterSubscriptionStmt; + _o.set(_j, "kind", _p?.kind); _o.set(_j, "subname", _p?.subname); _o.set(_j, "conninfo", _p?.conninfo); _o.set(_j, "publication", _p?.publication); _o.set(_j, "options", _p?.options); + return { AlterSubscriptionStmt: _j }; @@ -3552,9 +4087,11 @@ export default { DropSubscriptionStmt: DropSubscriptionStmt; } { const _j = {} as DropSubscriptionStmt; + _o.set(_j, "subname", _p?.subname); _o.set(_j, "missing_ok", _p?.missing_ok); _o.set(_j, "behavior", _p?.behavior); + return { DropSubscriptionStmt: _j }; @@ -3563,10 +4100,12 @@ export default { ScanToken: ScanToken; } { const _j = {} as ScanToken; + _o.set(_j, "start", _p?.start); _o.set(_j, "end", _p?.end); _o.set(_j, "token", _p?.token); _o.set(_j, "keywordKind", _p?.keywordKind); + return { ScanToken: _j }; diff --git a/postgres/pg-cache/src/lru.ts b/postgres/pg-cache/src/lru.ts index 119fb37af..2ea9aa9cf 100644 --- a/postgres/pg-cache/src/lru.ts +++ b/postgres/pg-cache/src/lru.ts @@ -63,6 +63,7 @@ export class PgPoolCacheManager { // Register a cleanup callback to be called when pools are disposed registerCleanupCallback(callback: PoolCleanupCallback): () => void { this.cleanupCallbacks.add(callback); + // Return unregister function return () => { this.cleanupCallbacks.delete(callback); @@ -85,6 +86,7 @@ export class PgPoolCacheManager { delete(key: PgPoolKey): void { const managedPool = this.pgCache.get(key); const existed = this.pgCache.delete(key); + if (!existed && managedPool) { this.notifyCleanup(key); this.disposePool(managedPool); @@ -93,6 +95,7 @@ export class PgPoolCacheManager { clear(): void { const entries = [...this.pgCache.entries()]; + this.pgCache.clear(); for (const [key, managedPool] of entries) { this.notifyCleanup(key); @@ -110,6 +113,7 @@ export class PgPoolCacheManager { async waitForDisposals(): Promise { if (this.cleanupTasks.length === 0) return; const tasks = [...this.cleanupTasks]; + this.cleanupTasks = []; await Promise.allSettled(tasks); } @@ -127,6 +131,7 @@ export class PgPoolCacheManager { private disposePool(managedPool: ManagedPgPool): void { if (managedPool.isDisposed) return; const task = managedPool.dispose(); + this.cleanupTasks.push(task); } } diff --git a/postgres/pg-cache/src/pg.ts b/postgres/pg-cache/src/pg.ts index 5bd9d3727..ca9a20b0f 100644 --- a/postgres/pg-cache/src/pg.ts +++ b/postgres/pg-cache/src/pg.ts @@ -1,6 +1,6 @@ +import { Logger } from '@pgpmjs/logger'; import pg from 'pg'; import { getPgEnvOptions, PgConfig } from 'pg-env'; -import { Logger } from '@pgpmjs/logger'; import { pgCache } from './lru'; @@ -18,8 +18,10 @@ const getDbString = ( export const getPgPool = (pgConfig: Partial): pg.Pool => { const config = getPgEnvOptions(pgConfig); const { user, password, host, port, database, } = config; + if (pgCache.has(database)) { const cached = pgCache.get(database); + if (cached) return cached; } const connectionString = getDbString(user, password, host, port, database); @@ -78,5 +80,6 @@ export const getPgPool = (pgConfig: Partial): pg.Pool => { }); pgCache.set(database, pgPool); + return pgPool; }; diff --git a/postgres/pg-codegen/__tests__/codegen.test.ts b/postgres/pg-codegen/__tests__/codegen.test.ts index 545abee65..44ea515ae 100644 --- a/postgres/pg-codegen/__tests__/codegen.test.ts +++ b/postgres/pg-codegen/__tests__/codegen.test.ts @@ -56,6 +56,7 @@ it('generates _common.ts with UUID and Timestamp types', async () => { }); const common = output['schemas/_common.ts']; + expect(common).toMatchSnapshot(); }); @@ -76,6 +77,7 @@ it('generates interfaces and classes for tables', async () => { }); const schema = output['schemas/codegen_test.ts']; + expect(schema).toMatchSnapshot(); }); @@ -96,6 +98,7 @@ it('imports UUID and Timestamp when used in schema', async () => { }); const schema = output['schemas/codegen_test.ts']; + expect(schema).toMatch(/import\s+\{\s*UUID,\s*Timestamp\s*\}\s+from ["']\.\/_common["']/); }); @@ -116,5 +119,6 @@ it('generates an index.ts that exports schema namespace', async () => { }); const index = output['index.ts']; + expect(index).toMatchSnapshot(); }); diff --git a/postgres/pg-codegen/src/codegen/codegen.ts b/postgres/pg-codegen/src/codegen/codegen.ts index d57047bfd..3db2ab51c 100644 --- a/postgres/pg-codegen/src/codegen/codegen.ts +++ b/postgres/pg-codegen/src/codegen/codegen.ts @@ -17,6 +17,7 @@ export const generateCodeTree = ( // Common types const commonTypes: t.Statement[] = []; + if (includeTimestamps) { commonTypes.push( t.exportNamedDeclaration( @@ -48,6 +49,7 @@ export const generateCodeTree = ( if (attr.kind === 'attribute' && attr.classId === obj.id) { const fieldType = mapPostgresTypeToTSType(attr.typeId, attr.isNotNull); const postgresType = mapPostgresTypeToIdentifier(attr.typeId); + if (postgresType) usedTypes.add(postgresType); interfaceFields.push( @@ -80,6 +82,7 @@ export const generateCodeTree = ( ); const data = t.identifier('data'); + data.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(pascalName))); const classImplements = t.tsExpressionWithTypeArguments(t.identifier(pascalName)); @@ -93,9 +96,11 @@ export const generateCodeTree = ( ]) ) ); + (classDeclaration.declaration as t.ClassDeclaration).implements = [classImplements]; const filePath = `schemas/${schemaName}.ts`; + if (!schemaFiles[filePath]) schemaFiles[filePath] = []; if (usedTypes.size > 0) { @@ -114,6 +119,7 @@ export const generateCodeTree = ( ); } else { const current = new Set(existingImports.specifiers.map((s) => (s.local as t.Identifier).name)); + Array.from(usedTypes).forEach((type) => { if (!current.has(type)) { existingImports.specifiers.push( @@ -130,8 +136,10 @@ export const generateCodeTree = ( // index.ts exports const indexFileStatements: t.Statement[] = []; + Object.keys(schemaFiles).forEach((filePath) => { const schemaName = filePath.replace('schemas/', '').replace('.ts', ''); + if (schemaName === '_common') return; if (schemaName === 'public') { @@ -152,6 +160,7 @@ export const generateCodeTree = ( }); const fileTree: Record = {}; + Object.entries(schemaFiles).forEach(([filePath, statements]) => { // @ts-ignore fileTree[filePath] = generate(t.program(statements)).code; @@ -159,6 +168,7 @@ export const generateCodeTree = ( // @ts-ignore fileTree['index.ts'] = generate(t.program(indexFileStatements)).code; + return fileTree; }; @@ -170,6 +180,7 @@ const toPascalCase = (str: string): string => const mapPostgresTypeToTSType = (typeId: string, isNotNull: boolean): t.TSType => { const optionalType = (type: t.TSType): t.TSType => isNotNull ? type : t.tsUnionType([type, t.tsNullKeyword()]); + switch (typeId) { case '20': // BIGINT case '21': // SMALLINT diff --git a/postgres/pg-codegen/src/codegen/namespaces.ts b/postgres/pg-codegen/src/codegen/namespaces.ts index cf59f4a00..4cc3d0fed 100644 --- a/postgres/pg-codegen/src/codegen/namespaces.ts +++ b/postgres/pg-codegen/src/codegen/namespaces.ts @@ -79,6 +79,7 @@ export const generateCode = (databaseObjects: DatabaseObject[], options: Codegen t.identifier(namespace), t.tsModuleBlock(namespaces[namespace]) ); + namespaceDeclaration.declare = true; programBody.push(namespaceDeclaration); }); diff --git a/postgres/pg-codegen/src/index.ts b/postgres/pg-codegen/src/index.ts index 939016167..be75e92c2 100644 --- a/postgres/pg-codegen/src/index.ts +++ b/postgres/pg-codegen/src/index.ts @@ -31,6 +31,7 @@ const writeGeneratedFiles = async ( // Ensure the directory for the file exists const dirName = path.dirname(fullPath); + await fs.mkdir(dirName, { recursive: true }); // Write the file content @@ -65,6 +66,7 @@ const writeGeneratedFiles = async ( // Fetch introspection rows const rows: DatabaseObject[] = await getIntrospectionRows(options); + log.info('Introspection Rows Fetched:', rows); // Generate TypeScript code diff --git a/postgres/pg-codegen/src/query.ts b/postgres/pg-codegen/src/query.ts index 5beda1c54..59dc16e8b 100644 --- a/postgres/pg-codegen/src/query.ts +++ b/postgres/pg-codegen/src/query.ts @@ -21,6 +21,7 @@ export const makeIntrospectionQuery = (serverVersionNum: number, options: Intros where pg_auth_members.roleid = pg_roles.oid and pg_auth_members.member = accessible_roles._oid `; + return `\ -- @see https://www.postgresql.org/docs/9.5/static/catalogs.html -- @see https://github.com/graphile/graphile-engine/blob/master/packages/graphile-build-pg/src/plugins/introspectionQuery.js diff --git a/postgres/pg-env/src/env.ts b/postgres/pg-env/src/env.ts index ef7a83081..6627828c0 100644 --- a/postgres/pg-env/src/env.ts +++ b/postgres/pg-env/src/env.ts @@ -2,6 +2,7 @@ import { defaultPgConfig,PgConfig } from './pg-config'; const parseEnvNumber = (val?: string): number | undefined => { const num = Number(val); + return !isNaN(num) ? num : undefined; }; @@ -26,11 +27,13 @@ export const getPgEnvVars = (): Partial => { export const getPgEnvOptions = (overrides: Partial = {}): PgConfig => { const envOpts = getPgEnvVars(); const merged = { ...defaultPgConfig, ...envOpts, ...overrides }; + return merged; }; export function toPgEnvVars(config: Partial): Record { const opts = { ...defaultPgConfig, ...config }; + return { ...(opts.host && { PGHOST: opts.host }), ...(opts.port && { PGPORT: String(opts.port) }), diff --git a/postgres/pg-query-context/src/index.ts b/postgres/pg-query-context/src/index.ts index 6ec0cee1a..6e713e8fa 100644 --- a/postgres/pg-query-context/src/index.ts +++ b/postgres/pg-query-context/src/index.ts @@ -3,12 +3,14 @@ import { ClientBase, Pool, PoolClient, QueryResult } from 'pg'; function setContext(ctx: Record): string[] { return Object.keys(ctx || {}).reduce((m, el) => { m.push(`SELECT set_config('${el}', '${ctx[el]}', true);`); + return m; }, []); } async function execContext(client: ClientBase, ctx: Record): Promise { const local = setContext(ctx); + for (const query of local) { await client.query(query); } @@ -32,6 +34,7 @@ export default async ({ client, context = {}, query = '', variables = [] }: Exec await pgClient.query('BEGIN'); await execContext(pgClient, context); const result = await pgClient.query(query, variables); + await pgClient.query('COMMIT'); return result; diff --git a/postgres/pgsql-test/__tests__/postgres-test.auth-methods.test.ts b/postgres/pgsql-test/__tests__/postgres-test.auth-methods.test.ts index e31ab2ca4..6a49879ff 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.auth-methods.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.auth-methods.test.ts @@ -40,17 +40,21 @@ describe('auth() method', () => { it('sets role and userId', async () => { const authRole = getRoleName('authenticated'); + db.auth({ role: authRole, userId: '12345' }); const role = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(role.rows[0].role).toBe(authRole); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('12345'); }); it('sets role with custom userIdKey', async () => { const authRole = getRoleName('authenticated'); + db.auth({ role: authRole, userId: 'custom-123', @@ -58,25 +62,31 @@ describe('auth() method', () => { }); const role = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(role.rows[0].role).toBe(authRole); const userId = await db.query('SELECT current_setting(\'app.user.id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('custom-123'); }); it('handles numeric userId', async () => { const authRole = getRoleName('authenticated'); + db.auth({ role: authRole, userId: 99999 }); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('99999'); }); it('sets role without userId', async () => { const authRole = getRoleName('authenticated'); + db.auth({ role: authRole }); const role = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(role.rows[0].role).toBe(authRole); }); }); @@ -95,20 +105,25 @@ describe('auth() with default role', () => { const role = await db.query('SELECT current_setting(\'role\', true) AS role'); const expectedRole = getRoleName('authenticated'); + expect(role.rows[0].role).toBe(expectedRole); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('test-user-456'); }); it('allows explicit role override', async () => { const anonRole = getRoleName('anonymous'); + db.auth({ userId: 'admin-user-789', role: anonRole }); const role = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(role.rows[0].role).toBe(anonRole); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('admin-user-789'); }); @@ -116,6 +131,7 @@ describe('auth() with default role', () => { db.auth({ userId: 42 }); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('42'); }); }); @@ -131,6 +147,7 @@ describe('clearContext() method', () => { it('clears all session variables', async () => { const authRole = getRoleName('authenticated'); + db.setContext({ role: authRole, 'jwt.claims.user_id': 'test-123', @@ -139,36 +156,43 @@ describe('clearContext() method', () => { }); const roleBefore = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(roleBefore.rows[0].role).toBe(authRole); const userIdBefore = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userIdBefore.rows[0].user_id).toBe('test-123'); db.clearContext(); const defaultRole = getRoleName('anonymous'); const roleAfter = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(roleAfter.rows[0].role).toBe(defaultRole); const userIdAfter = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userIdAfter.rows[0].user_id).toBe(''); }); it('allows setting new context after clearing', async () => { const authRole = getRoleName('authenticated'); + db.setContext({ role: authRole, 'jwt.claims.user_id': 'old-user' }); const userIdBefore = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userIdBefore.rows[0].user_id).toBe('old-user'); db.clearContext(); db.auth({ userId: 'new-user' }); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('new-user'); }); }); @@ -188,11 +212,13 @@ describe('publish() method', () => { await pg.query(`INSERT INTO test_users (email, name) VALUES ('alice@test.com', 'Alice')`); const beforePublish = await db.any('SELECT * FROM test_users WHERE email = $1', ['alice@test.com']); + expect(beforePublish.length).toBe(0); await pg.publish(); const afterPublish = await db.any('SELECT * FROM test_users WHERE email = $1', ['alice@test.com']); + expect(afterPublish.length).toBe(1); expect(afterPublish[0].email).toBe('alice@test.com'); expect(afterPublish[0].name).toBe('Alice'); @@ -200,14 +226,17 @@ describe('publish() method', () => { it('preserves context after publish', async () => { const authRole = getRoleName('authenticated'); + db.auth({ role: authRole, userId: 'test-999' }); await db.publish(); const role = await db.query('SELECT current_setting(\'role\', true) AS role'); + expect(role.rows[0].role).toBe(authRole); const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); + expect(userId.rows[0].user_id).toBe('test-999'); }); @@ -218,11 +247,13 @@ describe('publish() method', () => { await pg.query(`INSERT INTO test_users (email, name) VALUES ('charlie@test.com', 'Charlie')`); const beforeRollback = await pg.any('SELECT * FROM test_users WHERE email IN ($1, $2)', ['bob@test.com', 'charlie@test.com']); + expect(beforeRollback.length).toBe(2); await pg.rollback(); // Rollback Charlie to the savepoint created by publish() (important-comment) const afterRollback = await pg.any('SELECT * FROM test_users WHERE email IN ($1, $2)', ['bob@test.com', 'charlie@test.com']); + expect(afterRollback.length).toBe(1); // Only Bob remains (was committed via publish) (important-comment) expect(afterRollback[0].email).toBe('bob@test.com'); diff --git a/postgres/pgsql-test/__tests__/postgres-test.connections.test.ts b/postgres/pgsql-test/__tests__/postgres-test.connections.test.ts index 4833e42c6..73d14fb41 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.connections.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.connections.test.ts @@ -27,6 +27,7 @@ describe('anonymous', () => { it('runs under anonymous context', async () => { const result = await db.query('SELECT current_setting(\'role\', true) AS role'); const expectedRole = getRoleName('anonymous'); + expect(result.rows[0].role).toBe(expectedRole); }); }); @@ -34,6 +35,7 @@ describe('anonymous', () => { describe('authenticated', () => { beforeEach(async () => { const authRole = getRoleName('authenticated'); + db.setContext({ role: authRole, 'jwt.claims.user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', @@ -51,8 +53,10 @@ describe('authenticated', () => { it('runs under authenticated context', async () => { const role = await db.query('SELECT current_setting(\'role\', true) AS role'); const expectedRole = getRoleName('authenticated'); + expect(role.rows[0].role).toBe(expectedRole); const ip = await db.query('SELECT current_setting(\'jwt.claims.ip_address\', true) AS role'); + expect(ip.rows[0].role).toBe('127.0.0.1'); }); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.csv-subset-header.test.ts b/postgres/pgsql-test/__tests__/postgres-test.csv-subset-header.test.ts index b6b31dfd4..8be8eeaaf 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.csv-subset-header.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.csv-subset-header.test.ts @@ -73,6 +73,7 @@ it('csv in/out', async () => { // 6. Ensure output directory exists const outDir = path.resolve(__dirname, '../output'); + fs.mkdirSync(outDir, { recursive: true }); // 7. Export updated tables to CSV diff --git a/postgres/pgsql-test/__tests__/postgres-test.csv.test.ts b/postgres/pgsql-test/__tests__/postgres-test.csv.test.ts index ad87b7a9a..d06785039 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.csv.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.csv.test.ts @@ -77,6 +77,7 @@ it('csv in/out', async () => { // 6. Ensure output directory exists const outDir = path.resolve(__dirname, '../output'); + fs.mkdirSync(outDir, { recursive: true }); // 7. Export updated tables to CSV diff --git a/postgres/pgsql-test/__tests__/postgres-test.deploy-fast.test.ts b/postgres/pgsql-test/__tests__/postgres-test.deploy-fast.test.ts index 3a904f14d..45a73c7a5 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.deploy-fast.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.deploy-fast.test.ts @@ -18,7 +18,8 @@ let totalStart: number; beforeAll(async () => { totalStart = Date.now(); - const cwd = resolve(__dirname + '/../../../__fixtures__/sqitch/simple/packages/my-third'); + const cwd = resolve(`${__dirname }/../../../__fixtures__/sqitch/simple/packages/my-third`); + ({ pg, teardown } = await getConnections({}, [ seed.pgpm(cwd) ])); @@ -34,6 +35,7 @@ beforeEach(async () => { afterEach(async () => { const elapsed = Date.now() - start; const name = expect.getState().currentTestName ?? 'unknown'; + testResults.push({ name, time: elapsed }); await pg.afterEach(); @@ -56,24 +58,27 @@ afterAll(async () => { `πŸ•’ Total Test Time: ${totalTime}ms` ]; - console.log('\n' + summaryLines.join('\n') + '\n'); + console.log(`\n${ summaryLines.join('\n') }\n`); }); describe('Constructive DeployFast DB Benchmark', () => { it('inserts Alice', async () => { await pg.query(`INSERT INTO myfirstapp.users (username, email) VALUES ('alice', 'alice@example.com')`); const res = await pg.query(`SELECT COUNT(*) FROM myfirstapp.users`); + expect(res.rows[0].count).toBe('1'); }); it('starts clean without Alice', async () => { const res = await pg.query(`SELECT * FROM myfirstapp.users WHERE username = 'alice'`); + expect(res.rows).toHaveLength(0); }); it('inserts Bob with email', async () => { await pg.query(`INSERT INTO myfirstapp.users (username, email) VALUES ('bob', 'bob@example.com')`); const res = await pg.query(`SELECT * FROM myfirstapp.users WHERE username = 'bob'`); + expect(res.rows[0].email).toBe('bob@example.com'); }); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.grants.test.ts b/postgres/pgsql-test/__tests__/postgres-test.grants.test.ts index be16dac3e..330d009c0 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.grants.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.grants.test.ts @@ -19,6 +19,7 @@ const sqlPath = (file: string) => resolve(__dirname, '../sql', file); */ function setupBaseDatabase(config: PgConfig) { const admin = new DbAdmin(config); + admin.loadSql(sqlPath('test.sql'), config.database); } @@ -41,11 +42,13 @@ afterAll(async () => { describe('Database Setup', () => { it('has valid connection (pg)', async () => { const result = await pg.query('SELECT 1 AS ok'); + expect(result.rows[0].ok).toBe(1); }); it('has valid connection (db)', async () => { const result = await db.query('SELECT 2 AS ok'); + expect(result.rows[0].ok).toBe(2); }); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.records.test.ts b/postgres/pgsql-test/__tests__/postgres-test.records.test.ts index 060a1e241..595a80c8a 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.records.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.records.test.ts @@ -47,18 +47,21 @@ describe('Postgres Test Framework', () => { it('should have 2 users initially', async () => { const { rows } = await pg.query('SELECT COUNT(*) FROM users'); + expect(rows[0].count).toBe('2'); }); it('inserts a user but rollback leaves baseline intact', async () => { await pg.query(`INSERT INTO users (name) VALUES ('Carol')`); - let res = await pg.query('SELECT COUNT(*) FROM users'); + const res = await pg.query('SELECT COUNT(*) FROM users'); + expect(res.rows[0].count).toBe('3'); // inside this tx // after rollback... the next test, we’ll still see 2 }); it('still sees 2 users after previous insert test', async () => { const { rows } = await pg.query('SELECT COUNT(*) FROM users'); + expect(rows[0].count).toBe('2'); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.rollbacks.test.ts b/postgres/pgsql-test/__tests__/postgres-test.rollbacks.test.ts index 08856e389..7bda9020f 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.rollbacks.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.rollbacks.test.ts @@ -37,6 +37,7 @@ beforeEach(async () => { afterEach(async () => { const elapsed = Date.now() - start; const name = expect.getState().currentTestName ?? 'unknown'; + testResults.push({ name, time: elapsed }); await pg.afterEach(); @@ -58,24 +59,27 @@ afterAll(async () => { `πŸ•’ Total Test Time: ${totalTime}ms` ]; - console.log('\n' + summaryLines.join('\n') + '\n'); + console.log(`\n${ summaryLines.join('\n') }\n`); }); describe('Rollback DB Benchmark', () => { it('inserts Alice', async () => { await pg.query(`INSERT INTO app_public.users (username) VALUES ('alice')`); const res = await pg.query(`SELECT COUNT(*) FROM app_public.users`); + expect(res.rows[0].count).toBe('1'); }); it('starts clean without Alice', async () => { const res = await pg.query(`SELECT * FROM app_public.users WHERE username = 'alice'`); + expect(res.rows).toHaveLength(0); }); it('inserts Bob, settings should be empty', async () => { await pg.query(`INSERT INTO app_public.users (username) VALUES ('bob')`); const settings = await pg.query(`SELECT * FROM app_public.user_settings`); + expect(settings.rows).toHaveLength(0); }); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.seed-methods.test.ts b/postgres/pgsql-test/__tests__/postgres-test.seed-methods.test.ts index 550e86e62..ecc2d2e40 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.seed-methods.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.seed-methods.test.ts @@ -1,7 +1,8 @@ process.env.LOG_SCOPE = 'pgsql-test'; -import { writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; +import { mkdirSync,writeFileSync } from 'fs'; import { tmpdir } from 'os'; +import { join } from 'path'; + import { seed } from '../src'; import { getConnections } from '../src/connect'; import { PgTestClient } from '../src/test-client'; @@ -93,6 +94,7 @@ describe('loadJson() method', () => { }); const result = await pg.any('SELECT * FROM custom.users ORDER BY name'); + expect(result).toHaveLength(2); expect(result[0].name).toBe('Alice'); expect(result[1].name).toBe('Bob'); @@ -107,6 +109,7 @@ describe('loadJson() method', () => { }); const result = await db.any('SELECT * FROM custom.users'); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Charlie'); }); @@ -133,7 +136,8 @@ describe('loadJson() method', () => { describe('loadCsv() method', () => { it('should load users from CSV with pg client', async () => { const csvPath = join(testDir, 'users.csv'); - const csvContent = 'id,name\n' + [users[0], users[1]].map(u => `${u.id},${u.name}`).join('\n') + '\n'; + const csvContent = `id,name\n${ [users[0], users[1]].map(u => `${u.id},${u.name}`).join('\n') }\n`; + writeFileSync(csvPath, csvContent); await pg.loadCsv({ @@ -141,6 +145,7 @@ describe('loadCsv() method', () => { }); const result = await pg.any('SELECT * FROM custom.users ORDER BY name'); + expect(result).toHaveLength(2); expect(result[0].name).toBe('Alice'); expect(result[1].name).toBe('Bob'); @@ -152,6 +157,7 @@ describe('loadCsv() method', () => { const csvPath = join(testDir, 'users2.csv'); const csvContent = 'id,name\n' + `${users[2].id},${users[2].name}\n`; + writeFileSync(csvPath, csvContent); // COPY FROM is not supported with row-level security @@ -167,7 +173,7 @@ describe('loadCsv() method', () => { const petsPath = join(testDir, 'pets.csv'); writeFileSync(usersPath, `id,name\n${users[0].id},${users[0].name}\n`); - writeFileSync(petsPath, 'id,owner_id,name\n' + [pets[0], pets[1]].map(p => `${p.id},${p.owner_id},${p.name}`).join('\n') + '\n'); + writeFileSync(petsPath, `id,owner_id,name\n${ [pets[0], pets[1]].map(p => `${p.id},${p.owner_id},${p.name}`).join('\n') }\n`); await pg.loadCsv({ 'custom.users': usersPath, @@ -190,11 +196,13 @@ describe('loadSql() method', () => { const sqlContent = [users[0], users[1]] .map(u => `INSERT INTO custom.users (id, name) VALUES ('${u.id}', '${u.name}');`) .join('\n'); + writeFileSync(sqlPath, sqlContent); await pg.loadSql([sqlPath]); const result = await pg.any('SELECT * FROM custom.users ORDER BY name'); + expect(result).toHaveLength(2); expect(result[0].name).toBe('Alice'); expect(result[1].name).toBe('Bob'); @@ -205,11 +213,13 @@ describe('loadSql() method', () => { await db.beforeEach(); const sqlPath = join(testDir, 'seed2.sql'); + writeFileSync(sqlPath, `INSERT INTO custom.users (id, name) VALUES ('${users[2].id}', '${users[2].name}');`); await db.loadSql([sqlPath]); const result = await db.any('SELECT * FROM custom.users'); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Charlie'); }); @@ -222,6 +232,7 @@ describe('loadSql() method', () => { const petsSql = [pets[0], pets[1]] .map(p => `INSERT INTO custom.pets (id, owner_id, name) VALUES (${p.id}, '${p.owner_id}', '${p.name}');`) .join('\n'); + writeFileSync(petsPath, petsSql); await pg.loadSql([usersPath, petsPath]); @@ -264,6 +275,7 @@ describe('cross-connection visibility with publish()', () => { await db.beforeEach(); const result = await db.any('SELECT * FROM custom.users'); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Alice'); }); @@ -280,6 +292,7 @@ describe('cross-connection visibility with publish()', () => { // pg client (superuser) can see all data const result = await pg.any('SELECT * FROM custom.users'); + expect(result).toHaveLength(1); expect(result[0].name).toBe('Bob'); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.template.test.ts b/postgres/pgsql-test/__tests__/postgres-test.template.test.ts index aee31ecab..07e4cf6d8 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.template.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.template.test.ts @@ -28,6 +28,7 @@ function setupTemplateDatabase(): void { }); const admin = new DbAdmin(templateConfig); + admin.cleanupTemplate(TEMPLATE_NAME); admin.createSeededTemplate(TEMPLATE_NAME, { seed: (ctx) => { @@ -55,6 +56,7 @@ afterEach(async () => { await teardown(); const elapsed = Date.now() - start; const name = expect.getState().currentTestName ?? 'unknown'; + testResults.push({ name, time: elapsed }); }); @@ -74,24 +76,27 @@ afterAll(() => { `πŸ•’ Total Test Time: ${totalTime}ms` ]; - console.log('\n' + summaryLines.join('\n') + '\n'); + console.log(`\n${ summaryLines.join('\n') }\n`); }); describe('Template DB Benchmark', () => { it('inserts Alice', async () => { await pg.query(`INSERT INTO app_public.users (username) VALUES ('alice')`); const res = await pg.query(`SELECT COUNT(*) FROM app_public.users`); + expect(res.rows[0].count).toBe('1'); }); it('starts clean without Alice', async () => { const res = await pg.query(`SELECT * FROM app_public.users WHERE username = 'alice'`); + expect(res.rows).toHaveLength(0); }); it('inserts Bob, settings should be empty', async () => { await pg.query(`INSERT INTO app_public.users (username) VALUES ('bob')`); const settings = await pg.query(`SELECT * FROM app_public.user_settings`); + expect(settings.rows).toHaveLength(0); }); }); diff --git a/postgres/pgsql-test/__tests__/postgres-test.transations.test.ts b/postgres/pgsql-test/__tests__/postgres-test.transations.test.ts index dd7c5fecb..b3ab188d6 100644 --- a/postgres/pgsql-test/__tests__/postgres-test.transations.test.ts +++ b/postgres/pgsql-test/__tests__/postgres-test.transations.test.ts @@ -71,6 +71,7 @@ describe('Manual rollback using app_public.users', () => { const res = await pg.query(` SELECT * FROM app_public.users WHERE username = 'zara' `); + expect(res.rows).toHaveLength(0); // proves rollback works }); @@ -105,6 +106,7 @@ describe('Manual rollback using app_public.users', () => { // Insert a user that we’ll intentionally roll back await pg.query(`INSERT INTO app_public.users (username) VALUES ('temp_user')`); const beforeRollback = await pg.query(`SELECT * FROM app_public.users WHERE username = 'temp_user'`); + expect(beforeRollback.rows).toHaveLength(1); // Midway rollback (manual!) @@ -117,6 +119,7 @@ describe('Manual rollback using app_public.users', () => { // Now insert another user instead await pg.query(`INSERT INTO app_public.users (username) VALUES ('persistent_user')`); const afterRollback = await pg.query(`SELECT * FROM app_public.users WHERE username = 'persistent_user'`); + expect(afterRollback.rows).toHaveLength(1); }); @@ -132,6 +135,7 @@ describe('Manual rollback using app_public.users', () => { it('verifies crash_test user was cleaned up after failure', async () => { const res = await pg.query(`SELECT * FROM app_public.users WHERE username = 'crash_test'`); + expect(res.rows).toHaveLength(0); // proves rollback on failed test worked }); }); diff --git a/postgres/pgsql-test/__tests__/utils.test.ts b/postgres/pgsql-test/__tests__/utils.test.ts index 9f75dda8c..86eca79d7 100644 --- a/postgres/pgsql-test/__tests__/utils.test.ts +++ b/postgres/pgsql-test/__tests__/utils.test.ts @@ -1,33 +1,36 @@ import { - snapshot, + IdHash, prune, pruneDates, - pruneIds, + pruneHashes, pruneIdArrays, + pruneIds, pruneUUIDs, - pruneHashes, - IdHash -} from '../src/utils'; + snapshot} from '../src/utils'; describe('snapshot utilities', () => { describe('pruneDates', () => { it('replaces Date objects with [DATE]', () => { const row = { created_at: new Date('2024-01-15'), name: 'Alice' }; + expect(pruneDates(row)).toEqual({ created_at: '[DATE]', name: 'Alice' }); }); it('replaces date strings in fields ending with _at', () => { const row = { updated_at: '2024-01-15T10:30:00Z', name: 'Bob' }; + expect(pruneDates(row)).toEqual({ updated_at: '[DATE]', name: 'Bob' }); }); it('replaces date strings in fields ending with At', () => { const row = { createdAt: '2024-01-15T10:30:00Z', name: 'Carol' }; + expect(pruneDates(row)).toEqual({ createdAt: '[DATE]', name: 'Carol' }); }); it('preserves null and undefined values', () => { const row: Record = { created_at: null, updated_at: undefined }; + expect(pruneDates(row)).toEqual({ created_at: null, updated_at: undefined }); }); }); @@ -35,16 +38,19 @@ describe('snapshot utilities', () => { describe('pruneIds', () => { it('replaces id field with [ID]', () => { const row = { id: 123, name: 'Alice' }; + expect(pruneIds(row)).toEqual({ id: '[ID]', name: 'Alice' }); }); it('replaces fields ending with _id', () => { const row = { user_id: 456, org_id: 'abc-123', name: 'Bob' }; + expect(pruneIds(row)).toEqual({ user_id: '[ID]', org_id: '[ID]', name: 'Bob' }); }); it('preserves null id values', () => { const row: Record = { id: null, user_id: undefined }; + expect(pruneIds(row)).toEqual({ id: null, user_id: undefined }); }); @@ -53,11 +59,13 @@ describe('snapshot utilities', () => { describe('pruneIdArrays', () => { it('replaces fields ending with _ids with [UUIDs-N]', () => { const row = { user_ids: ['a', 'b', 'c'], name: 'Alice' }; + expect(pruneIdArrays(row)).toEqual({ user_ids: '[UUIDs-3]', name: 'Alice' }); }); it('handles empty arrays', () => { const row: Record = { tag_ids: [] }; + expect(pruneIdArrays(row)).toEqual({ tag_ids: '[UUIDs-0]' }); }); }); @@ -65,21 +73,25 @@ describe('snapshot utilities', () => { describe('pruneUUIDs', () => { it('replaces uuid field with [UUID]', () => { const row = { uuid: '550e8400-e29b-41d4-a716-446655440000', name: 'Alice' }; + expect(pruneUUIDs(row)).toEqual({ uuid: '[UUID]', name: 'Alice' }); }); it('replaces queue_name field with [UUID]', () => { const row = { queue_name: '550e8400-e29b-41d4-a716-446655440000' }; + expect(pruneUUIDs(row)).toEqual({ queue_name: '[UUID]' }); }); it('replaces gravatar hash with [gUUID]', () => { const row = { gravatar: 'd41d8cd98f00b204e9800998ecf8427e' }; + expect(pruneUUIDs(row)).toEqual({ gravatar: '[gUUID]' }); }); it('does not replace non-UUID strings', () => { const row = { uuid: 'not-a-uuid', name: 'Bob' }; + expect(pruneUUIDs(row)).toEqual({ uuid: 'not-a-uuid', name: 'Bob' }); }); }); @@ -87,11 +99,13 @@ describe('snapshot utilities', () => { describe('pruneHashes', () => { it('replaces fields ending with _hash that start with $', () => { const row = { password_hash: '$2b$10$abc123', name: 'Alice' }; + expect(pruneHashes(row)).toEqual({ password_hash: '[hash]', name: 'Alice' }); }); it('does not replace hash fields not starting with $', () => { const row = { content_hash: 'abc123def456' }; + expect(pruneHashes(row)).toEqual({ content_hash: 'abc123def456' }); }); }); @@ -106,6 +120,7 @@ describe('snapshot utilities', () => { password_hash: '$2b$10$abc123', name: 'Alice' }; + expect(prune(row)).toEqual({ id: '[ID]', user_id: '[ID]', @@ -124,6 +139,7 @@ describe('snapshot utilities', () => { { id: 1, name: 'Alice', created_at: '2024-01-15T10:00:00Z' }, { id: 2, name: 'Bob', created_at: '2024-01-16T10:00:00Z' } ]; + expect(snapshot(data)).toEqual([ { id: '[ID]', name: 'Alice', created_at: '[DATE]' }, { id: '[ID]', name: 'Bob', created_at: '[DATE]' } @@ -140,6 +156,7 @@ describe('snapshot utilities', () => { } } }; + expect(snapshot(data)).toEqual({ user: { id: '[ID]', @@ -162,6 +179,7 @@ describe('snapshot utilities', () => { fetched_at: '2024-01-15T10:00:00Z' } }; + expect(snapshot(data)).toEqual({ users: [ { id: '[ID]', name: 'Alice' }, @@ -187,6 +205,7 @@ describe('snapshot utilities', () => { it('pruneIds uses IdHash to map IDs to numbered placeholders', () => { const idHash: IdHash = { 'uuid-1': 1, 'uuid-2': 2 }; const row = { id: 'uuid-1', user_id: 'uuid-2', org_id: 'unknown', name: 'Alice' }; + expect(pruneIds(row, idHash)).toEqual({ id: '[ID-1]', user_id: '[ID-2]', @@ -198,6 +217,7 @@ describe('snapshot utilities', () => { it('prune applies IdHash mapping when provided', () => { const idHash: IdHash = { '1': 1, '2': 2 }; const row = { id: 1, user_id: 2, name: 'Alice' }; + expect(prune(row, idHash)).toEqual({ id: '[ID-1]', user_id: '[ID-2]', @@ -215,6 +235,7 @@ describe('snapshot utilities', () => { { id: 'user-uuid-1', name: 'Alice', post_id: 'post-uuid-1' }, { id: 'user-uuid-2', name: 'Bob', post_id: 'post-uuid-1' } ]; + expect(snapshot(data, idHash)).toEqual([ { id: '[ID-1]', name: 'Alice', post_id: '[ID-3]' }, { id: '[ID-2]', name: 'Bob', post_id: '[ID-3]' } @@ -227,6 +248,7 @@ describe('snapshot utilities', () => { org: { id: 'org-1', name: 'Acme' }, user: { id: 'user-1', org_id: 'org-1' } }; + expect(snapshot(data, idHash)).toEqual({ org: { id: '[ID-1]', name: 'Acme' }, user: { id: '[ID-2]', org_id: '[ID-1]' } @@ -243,6 +265,7 @@ describe('snapshot utilities', () => { const data = [ { actor_id: 'uuid-user-2', entity_id: 'uuid-group-2', is_admin: true } ]; + expect(snapshot(data, idHash)).toEqual([ { actor_id: '[ID-user2]', entity_id: '[ID-group2]', is_admin: true } ]); diff --git a/postgres/pgsql-test/src/admin.ts b/postgres/pgsql-test/src/admin.ts index fde545cf6..b0b7378f6 100644 --- a/postgres/pgsql-test/src/admin.ts +++ b/postgres/pgsql-test/src/admin.ts @@ -70,11 +70,13 @@ export class DbAdmin { create(dbName?: string): void { const db = dbName ?? this.config.database; + this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} "${db}"`); } createFromTemplate(template: string, dbName?: string): void { const db = dbName ?? this.config.database; + this.run(`createdb -U ${this.config.user} -h ${this.config.host} -p ${this.config.port} -e "${db}" -T "${template}"`); } @@ -90,6 +92,7 @@ export class DbAdmin { connectionString(dbName?: string): string { const { user, password, host, port } = this.config; const db = dbName ?? this.config.database; + return `postgres://${user}:${password}@${host}:${port}/${db}`; } @@ -110,12 +113,14 @@ export class DbAdmin { async grantRole(role: string, user: string, dbName?: string): Promise { const db = dbName ?? this.config.database; const sql = generateGrantRoleSQL(role, user); + await this.streamSql(sql, db); } async grantConnect(role: string, dbName?: string): Promise { const db = dbName ?? this.config.database; const sql = `GRANT CONNECT ON DATABASE "${db}" TO ${role};`; + await this.streamSql(sql, db); } @@ -155,6 +160,7 @@ export class DbAdmin { async createSeededTemplate(templateName: string, adapter: SeedAdapter): Promise { const seedDb = this.config.database; + this.create(seedDb); await adapter.seed({ diff --git a/postgres/pgsql-test/src/connect.ts b/postgres/pgsql-test/src/connect.ts index 9f82b4512..88e89f9d2 100644 --- a/postgres/pgsql-test/src/connect.ts +++ b/postgres/pgsql-test/src/connect.ts @@ -2,7 +2,6 @@ import { getConnEnvOptions } from '@pgpmjs/env'; import { PgTestConnectionOptions } from '@pgpmjs/types'; import { randomUUID } from 'crypto'; import { teardownPgPools } from 'pg-cache'; - import { getPgEnvOptions, PgConfig, @@ -26,6 +25,7 @@ export const getPgRootAdmin = (config: PgConfig, connOpts: PgTestConnectionOptio database: connOpts.rootDb }); const admin = new DbAdmin(opts, false, connOpts); + return admin; }; @@ -40,6 +40,7 @@ const getConnOopts = (cn: GetConnectionOpts = {}) => { database: `${connect.prefix}${randomUUID()}`, ...cn.pg }); + return { pg: config, db: connect @@ -65,6 +66,7 @@ export const getConnections = async ( const connOpts: PgTestConnectionOptions = cn.db; const root = getPgRootAdmin(config, connOpts); + await root.createUserRole( connOpts.connections!.app!.user!, connOpts.connections!.app!.password!, @@ -95,6 +97,7 @@ export const getConnections = async ( await teardownPgPools(); await manager.closeAll(); })(); + return teardownPromise; }; @@ -103,12 +106,13 @@ export const getConnections = async ( await seed.compose(seedAdapters).seed({ connect: connOpts, admin, - config: config, + config, pg: manager.getClient(config) }); } catch (error) { const err: any = error as any; const msg = err && (err.stack || err.message) ? (err.stack || err.message) : String(err); + process.stderr.write(`[pgsql-test] Seed error (continuing): ${msg}\n`); // continue without teardown to allow caller-managed lifecycle } @@ -124,6 +128,7 @@ export const getConnections = async ( auth: connOpts.auth, roles: connOpts.roles }); + db.setContext({ role: getDefaultRole(connOpts) }); return { pg, db, teardown, manager, admin }; diff --git a/postgres/pgsql-test/src/context-utils.ts b/postgres/pgsql-test/src/context-utils.ts index f24824d15..b58836f41 100644 --- a/postgres/pgsql-test/src/context-utils.ts +++ b/postgres/pgsql-test/src/context-utils.ts @@ -14,6 +14,7 @@ export function generateContextStatements(context: PgTestClientContext): string return 'SET LOCAL ROLE NONE;'; } const escapedRole = val.replace(/"/g, '""'); + return `SET LOCAL ROLE "${escapedRole}";`; } // Use set_config for other context variables @@ -21,6 +22,7 @@ export function generateContextStatements(context: PgTestClientContext): string return `SELECT set_config('${key}', NULL, true);`; } const escapedVal = val.replace(/'/g, "''"); + return `SELECT set_config('${key}', '${escapedVal}', true);`; }) .join('\n'); diff --git a/postgres/pgsql-test/src/manager.ts b/postgres/pgsql-test/src/manager.ts index 5395838bc..658e14f65 100644 --- a/postgres/pgsql-test/src/manager.ts +++ b/postgres/pgsql-test/src/manager.ts @@ -13,6 +13,7 @@ const end = (pool: Pool) => { try { if ((pool as any).ended || (pool as any).ending) { log.warn('⚠️ pg pool already ended or ending'); + return; } pool.end(); @@ -49,6 +50,7 @@ export class PgTestConnector { if (!PgTestConnector.instance) { PgTestConnector.instance = new PgTestConnector(config, verbose); } + return PgTestConnector.instance; } @@ -71,6 +73,7 @@ export class PgTestConnector { private async awaitPendingConnects(): Promise { const arr = Array.from(this.pendingConnects); + if (arr.length) { await Promise.allSettled(arr); } @@ -78,11 +81,14 @@ export class PgTestConnector { getPool(config: PgConfig): Pool { const key = this.poolKey(config); + if (!this.pgPools.has(key)) { const pool = new Pool(config); + this.pgPools.set(key, pool); log.info(`πŸ“˜ Created new pg pool: ${key}`); } + return this.pgPools.get(key)!; } @@ -94,12 +100,15 @@ export class PgTestConnector { trackConnect: (p) => this.registerConnect(p), ...opts }); + this.clients.add(client); const key = this.dbKey(config); + this.seenDbConfigs.set(key, config); log.info(`πŸ”Œ New PgTestClient connected to ${config.database}`); + return client; } @@ -136,6 +145,7 @@ export class PgTestConnector { { ...config, user: rootPg.user, password: rootPg.password }, this.verbose ); + admin.drop(); log.warn(`🧨 Dropped database: ${config.database}`); } catch (err) { @@ -158,6 +168,7 @@ export class PgTestConnector { drop(config: PgConfig): void { const key = this.dbKey(config); const admin = new DbAdmin(config, this.verbose); + admin.drop(); log.warn(`🧨 Dropped database: ${config.database}`); this.seenDbConfigs.delete(key); diff --git a/postgres/pgsql-test/src/roles.ts b/postgres/pgsql-test/src/roles.ts index 4a36ba9c5..7eb5490d3 100644 --- a/postgres/pgsql-test/src/roles.ts +++ b/postgres/pgsql-test/src/roles.ts @@ -28,6 +28,7 @@ export const getRoleName = ( options?: PgTestConnectionOptions ): string => { const mapping = getRoleMapping(options); + return mapping[roleKey]; }; @@ -36,5 +37,6 @@ export const getRoleName = ( */ export const getDefaultRole = (options?: PgTestConnectionOptions): string => { const mapping = getRoleMapping(options); + return mapping.default; }; \ No newline at end of file diff --git a/postgres/pgsql-test/src/seed/csv.ts b/postgres/pgsql-test/src/seed/csv.ts index 4e39a1ce0..d601c9633 100644 --- a/postgres/pgsql-test/src/seed/csv.ts +++ b/postgres/pgsql-test/src/seed/csv.ts @@ -81,10 +81,12 @@ async function parseCsvHeader(filePath: string): Promise { parser.on('readable', () => { const row = parser.read() as string[] | null; + if (!row) return; if (row.length === 0) { cleanup(new Error('CSV header has no columns')); + return; } diff --git a/postgres/pgsql-test/src/seed/json.ts b/postgres/pgsql-test/src/seed/json.ts index c580d6ed7..18ab10676 100644 --- a/postgres/pgsql-test/src/seed/json.ts +++ b/postgres/pgsql-test/src/seed/json.ts @@ -1,4 +1,5 @@ import type { Client } from 'pg'; + import { SeedAdapter, SeedContext } from './types'; export interface JsonSeedMap { [table: string]: Record[]; @@ -23,6 +24,7 @@ export async function insertJson( for (const row of rows) { const values = columns.map((c) => row[c]); + await client.query(sql, values); } } @@ -42,6 +44,7 @@ export function json(data: JsonSeedMap): SeedAdapter { for (const row of rows) { const values = columns.map((c) => row[c]); + await pg.query(sql, values); } } diff --git a/postgres/pgsql-test/src/seed/pgpm.ts b/postgres/pgsql-test/src/seed/pgpm.ts index ab7dd2382..f81da031d 100644 --- a/postgres/pgsql-test/src/seed/pgpm.ts +++ b/postgres/pgsql-test/src/seed/pgpm.ts @@ -16,6 +16,7 @@ export async function deployPgpm( cache: boolean = false ): Promise { const proj = new PgpmPackage(cwd ?? process.cwd()); + if (!proj.isInModule()) return; await proj.deploy( diff --git a/postgres/pgsql-test/src/seed/sql.ts b/postgres/pgsql-test/src/seed/sql.ts index 3c4c01de7..453c77c63 100644 --- a/postgres/pgsql-test/src/seed/sql.ts +++ b/postgres/pgsql-test/src/seed/sql.ts @@ -16,6 +16,7 @@ export async function loadSqlFiles( } const sql = readFileSync(file, 'utf-8'); + await client.query(sql); } } diff --git a/postgres/pgsql-test/src/stream.ts b/postgres/pgsql-test/src/stream.ts index a7fce8364..be129528a 100644 --- a/postgres/pgsql-test/src/stream.ts +++ b/postgres/pgsql-test/src/stream.ts @@ -8,9 +8,11 @@ function setArgs(config: PgConfig): string[] { '-h', config.host, '-d', config.database ]; + if (config.port) { args.push('-p', String(config.port)); } + return args; } @@ -22,6 +24,7 @@ function stringToStream(text: string): Readable { this.push(null); } }); + return stream; } diff --git a/postgres/pgsql-test/src/test-client.ts b/postgres/pgsql-test/src/test-client.ts index 20593a372..48a06a6aa 100644 --- a/postgres/pgsql-test/src/test-client.ts +++ b/postgres/pgsql-test/src/test-client.ts @@ -1,12 +1,13 @@ +import { AuthOptions, PgTestClientContext,PgTestConnectionOptions } from '@pgpmjs/types'; import { Client, QueryResult } from 'pg'; import { PgConfig } from 'pg-env'; -import { AuthOptions, PgTestConnectionOptions, PgTestClientContext } from '@pgpmjs/types'; -import { getRoleName } from './roles'; + import { generateContextStatements } from './context-utils'; +import { getRoleName } from './roles'; +import { type CsvSeedMap,loadCsvMap } from './seed/csv'; import { insertJson, type JsonSeedMap } from './seed/json'; -import { loadCsvMap, type CsvSeedMap } from './seed/csv'; -import { loadSqlFiles } from './seed/sql'; import { deployPgpm } from './seed/pgpm'; +import { loadSqlFiles } from './seed/sql'; export type PgTestClientOpts = { deferConnect?: boolean; @@ -121,6 +122,7 @@ export class PgTestClient { const defaultRole = getRoleName('anonymous', this.opts); const nulledSettings: Record = {}; + Object.keys(this.contextSettings).forEach(key => { nulledSettings[key] = null; }); @@ -133,25 +135,31 @@ export class PgTestClient { async any(query: string, values?: any[]): Promise { const result = await this.query(query, values); + return result.rows; } async one(query: string, values?: any[]): Promise { const rows = await this.any(query, values); + if (rows.length !== 1) { throw new Error('Expected exactly one result'); } + return rows[0]; } async oneOrNone(query: string, values?: any[]): Promise { const rows = await this.any(query, values); + return rows[0] || null; } async many(query: string, values?: any[]): Promise { const rows = await this.any(query, values); + if (rows.length === 0) throw new Error('Expected many rows, got none'); + return rows; } @@ -178,6 +186,7 @@ export class PgTestClient { async query(query: string, values?: any[]): Promise> { await this.ctxQuery(); const result = await this.client.query(query, values); + return result; } diff --git a/postgres/pgsql-test/src/utils.ts b/postgres/pgsql-test/src/utils.ts index cbd0f437e..64d4114ed 100644 --- a/postgres/pgsql-test/src/utils.ts +++ b/postgres/pgsql-test/src/utils.ts @@ -8,6 +8,7 @@ const idReplacement = (v: unknown, idHash?: IdHash): string | unknown => { if (!v) return v; if (!idHash) return '[ID]'; const key = String(v); + return idHash[key] !== undefined ? `[ID-${idHash[key]}]` : '[ID]'; }; @@ -20,6 +21,7 @@ function mapValues( ): Record { return Object.entries(obj).reduce((acc, [key, value]) => { acc[key as keyof T] = fn(value, key as keyof T); + return acc; }, {} as Record); } @@ -31,13 +33,14 @@ export const pruneDates = (row: AnyObject): AnyObject => } if (v instanceof Date) { return '[DATE]'; - } else if ( + } if ( typeof v === 'string' && /(_at|At)$/.test(k as string) && /^20[0-9]{2}-[0-9]{2}-[0-9]{2}/.test(v) ) { return '[DATE]'; } + return v; }); @@ -67,6 +70,7 @@ export const pruneUUIDs = (row: AnyObject): AnyObject => if (k === 'gravatar' && /^[0-9a-f]{32}$/i.test(v)) { return '[gUUID]'; } + return v; }); @@ -120,6 +124,7 @@ export const defaultPruners: Pruner[] = [ // Compose pruners and apply pruneIds with IdHash support export const prune = (row: AnyObject, idHash?: IdHash): AnyObject => { const pruned = composePruners(...defaultPruners)(row); + return pruneIds(pruned, idHash); }; @@ -129,13 +134,16 @@ export const createSnapshot = (pruners: Pruner[]) => { const snap = (obj: unknown, idHash?: IdHash): unknown => { if (Array.isArray(obj)) { return obj.map((el) => snap(el, idHash)); - } else if (obj && typeof obj === 'object') { + } if (obj && typeof obj === 'object') { const pruned = pruneFn(obj as AnyObject); const prunedWithIds = pruneIds(pruned, idHash); + return mapValues(prunedWithIds, (v) => snap(v, idHash)); } + return obj; }; + return snap; }; diff --git a/postgres/supabase-test/src/connect.ts b/postgres/supabase-test/src/connect.ts index cefd52425..9c96894df 100644 --- a/postgres/supabase-test/src/connect.ts +++ b/postgres/supabase-test/src/connect.ts @@ -1,11 +1,10 @@ +import type { PgTestConnectionOptions } from '@pgpmjs/types'; import deepmerge from 'deepmerge'; import { getPgEnvVars, PgConfig } from 'pg-env'; import { - getConnections as getPgConnections, type GetConnectionOpts, - type GetConnectionResult -} from 'pgsql-test'; -import type { PgTestConnectionOptions } from '@pgpmjs/types'; + type GetConnectionResult, + getConnections as getPgConnections} from 'pgsql-test'; /** * Supabase default connection options diff --git a/postgres/supabase-test/src/helpers.ts b/postgres/supabase-test/src/helpers.ts index 1b5015ffb..04a0f521f 100644 --- a/postgres/supabase-test/src/helpers.ts +++ b/postgres/supabase-test/src/helpers.ts @@ -19,12 +19,13 @@ export async function insertUser( RETURNING id, email`, [id, email] ); - } else { + } + return await client.one( `INSERT INTO auth.users (id, email) VALUES (gen_random_uuid(), $1) RETURNING id, email`, [email] ); - } + } \ No newline at end of file diff --git a/postgres/supabase-test/src/index.ts b/postgres/supabase-test/src/index.ts index 6dd9ae370..00faf9b58 100644 --- a/postgres/supabase-test/src/index.ts +++ b/postgres/supabase-test/src/index.ts @@ -5,8 +5,8 @@ export * from 'pgsql-test'; export * from './helpers'; // Export Supabase-specific getConnections with defaults baked in -export { getConnections } from './connect'; export type { GetConnectionOpts, GetConnectionResult } from './connect'; +export { getConnections } from './connect'; // Re-export snapshot utility export { snapshot } from 'pgsql-test'; diff --git a/sandbox/my-third/__tests__/deploy-fast.test.ts b/sandbox/my-third/__tests__/deploy-fast.test.ts index 767195213..2bc431d84 100644 --- a/sandbox/my-third/__tests__/deploy-fast.test.ts +++ b/sandbox/my-third/__tests__/deploy-fast.test.ts @@ -1,18 +1,19 @@ import { deployFast, PgpmPackage } from '@pgpmjs/core'; -import { resolve } from 'path'; import { getEnvOptions } from '@pgpmjs/env'; -import { randomUUID } from 'crypto'; import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; +import { resolve } from 'path'; import { getPgPool } from 'pg-cache'; it('Constructive', async () => { - const db = 'db-'+randomUUID(); - const project = new PgpmPackage(resolve(__dirname+'/../')); + const db = `db-${randomUUID()}`; + const project = new PgpmPackage(resolve(`${__dirname}/../`)); const opts = getEnvOptions({ pg: { database: db } }) + execSync(`createdb ${opts.pg.database}`); await deployFast({ opts, @@ -24,6 +25,7 @@ it('Constructive', async () => { }); const pgPool = getPgPool({ ...opts.pg, database: db }); + await pgPool.end(); }); diff --git a/sandbox/my-third/__tests__/deploy-introspect.test.ts b/sandbox/my-third/__tests__/deploy-introspect.test.ts index 714d0d87f..86f28754a 100644 --- a/sandbox/my-third/__tests__/deploy-introspect.test.ts +++ b/sandbox/my-third/__tests__/deploy-introspect.test.ts @@ -1,17 +1,18 @@ import { deployFast, PgpmPackage } from '@pgpmjs/core'; import { getEnvOptions } from '@pgpmjs/env'; -import { getPgPool } from 'pg-cache'; -import { randomUUID } from 'crypto'; import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; +import { getPgPool } from 'pg-cache'; it('GraphQL query', async () => { - const newDb = 'db-constructive-'+randomUUID(); + const newDb = `db-constructive-${randomUUID()}`; const project = new PgpmPackage(process.env.CONSTRUCTIVE_WORKSPACE); const opts = getEnvOptions({ pg: { database: newDb } }) + execSync(`createdb ${opts.pg.database}`); await deployFast({ opts, diff --git a/sandbox/my-third/__tests__/deploy-massive.test.ts b/sandbox/my-third/__tests__/deploy-massive.test.ts index 2979e8c19..18394b655 100644 --- a/sandbox/my-third/__tests__/deploy-massive.test.ts +++ b/sandbox/my-third/__tests__/deploy-massive.test.ts @@ -1,15 +1,16 @@ import { deployFast, PgpmPackage } from '@pgpmjs/core'; import { getEnvOptions } from '@pgpmjs/env'; -import { randomUUID } from 'crypto'; import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; it('dashboard', async () => { const project = new PgpmPackage(process.env.CONSTRUCTIVE_DASHBOARD); const opts = getEnvOptions({ pg: { - database: 'db-dbe-'+randomUUID() + database: `db-dbe-${randomUUID()}` } }) + execSync(`createdb ${opts.pg.database}`); await deployFast({ opts, @@ -25,9 +26,10 @@ it('Constructive', async () => { const project = new PgpmPackage(process.env.CONSTRUCTIVE_WORKSPACE); const opts = getEnvOptions({ pg: { - database: 'db-constructive-'+randomUUID() + database: `db-constructive-${randomUUID()}` } }) + execSync(`createdb ${opts.pg.database}`); await deployFast({ opts, diff --git a/sandbox/my-third/__tests__/deploy-stream.test.ts b/sandbox/my-third/__tests__/deploy-stream.test.ts index 9611f83a3..f07cc7663 100644 --- a/sandbox/my-third/__tests__/deploy-stream.test.ts +++ b/sandbox/my-third/__tests__/deploy-stream.test.ts @@ -1,18 +1,19 @@ import { deployStream, PgpmPackage } from '@pgpmjs/core'; -import { resolve } from 'path'; import { getEnvOptions } from '@pgpmjs/env'; -import { randomUUID } from 'crypto'; import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; +import { resolve } from 'path'; import { getPgPool } from 'pg-cache'; it('Constructive', async () => { - const db = 'db-'+randomUUID(); - const project = new PgpmPackage(resolve(__dirname+'/../')); + const db = `db-${randomUUID()}`; + const project = new PgpmPackage(resolve(`${__dirname}/../`)); const opts = getEnvOptions({ pg: { database: db } }) + execSync(`createdb ${opts.pg.database}`); await deployStream({ opts, @@ -24,5 +25,6 @@ it('Constructive', async () => { }); const pgPool = getPgPool({ ...opts.pg, database: db }); + await pgPool.end(); }); diff --git a/sandbox/my-third/__tests__/deploy.test.ts b/sandbox/my-third/__tests__/deploy.test.ts index 51b878cc0..581c7e7bc 100644 --- a/sandbox/my-third/__tests__/deploy.test.ts +++ b/sandbox/my-third/__tests__/deploy.test.ts @@ -1,21 +1,24 @@ import { deploy, PgpmPackage } from '@pgpmjs/core'; -import { resolve } from 'path'; import { getEnvOptions } from '@pgpmjs/env'; -import { randomUUID } from 'crypto'; import { execSync } from 'child_process'; +import { randomUUID } from 'crypto'; +import { resolve } from 'path'; it('Constructive', async () => { - const project = new PgpmPackage(resolve(__dirname+'/../')); + const project = new PgpmPackage(resolve(`${__dirname}/../`)); + console.log(project); const plan = project.getModulePlan(); + console.log(plan); const opts = getEnvOptions({ pg: { - database: 'db-'+randomUUID() + database: `db-${randomUUID()}` } }) + execSync(`createdb ${opts.pg.database}`); await deploy(opts, 'my-third', opts.pg.database, project.modulePath); }); diff --git a/sandbox/ollama/__tests__/pgvector.test.ts b/sandbox/ollama/__tests__/pgvector.test.ts index 14ee01725..fa4358479 100644 --- a/sandbox/ollama/__tests__/pgvector.test.ts +++ b/sandbox/ollama/__tests__/pgvector.test.ts @@ -1,7 +1,8 @@ process.env.LOG_SCOPE = 'ollama'; jest.setTimeout(60000); -import { PgTestClient, getConnections } from 'pgsql-test'; +import { getConnections,PgTestClient } from 'pgsql-test'; + import { OllamaClient } from '../src/utils/ollama'; let pg: PgTestClient; @@ -63,10 +64,12 @@ describe('Vector Search with Document Chunks', () => { `SELECT id, content FROM intelligence.chunks WHERE document_id = $1 ORDER BY chunk_index`, [docId] ); + console.log(`Generated ${chunks.rows.length} chunks`); for (const chunk of chunks.rows) { const chunkEmbedding = await ollama.generateEmbedding(chunk.content); + await pg.client.query( 'UPDATE intelligence.chunks SET embedding = $1::vector WHERE id = $2', [formatVector(chunkEmbedding), chunk.id] @@ -77,6 +80,7 @@ describe('Vector Search with Document Chunks', () => { const chunksWithEmbeddings = await pg.client.query( 'SELECT COUNT(*) FROM intelligence.chunks WHERE embedding IS NOT NULL' ); + expect(Number(chunksWithEmbeddings.rows[0].count)).toBeGreaterThan(0); // 6. Search for similar chunks using a query diff --git a/sandbox/ollama/__tests__/rag.test.ts b/sandbox/ollama/__tests__/rag.test.ts index 39c720c42..a0812f68e 100644 --- a/sandbox/ollama/__tests__/rag.test.ts +++ b/sandbox/ollama/__tests__/rag.test.ts @@ -1,9 +1,10 @@ process.env.LOG_SCOPE = 'ollama'; jest.setTimeout(60000); -import { PgTestClient, getConnections } from 'pgsql-test'; -import { OllamaClient } from '../src/utils/ollama'; import fetch from 'cross-fetch'; +import { getConnections,PgTestClient } from 'pgsql-test'; + +import { OllamaClient } from '../src/utils/ollama'; let pg: PgTestClient; let teardown: () => Promise; @@ -22,7 +23,9 @@ const measureTime = async (service: 'ollama' | 'postgres' | 'other', action: const start = performance.now(); const result = await fn(); const end = performance.now(); + addLog(service, action, end - start); + return result; }; @@ -98,6 +101,7 @@ describe('Retrieval Augmented Generation (RAG)', () => { // Process chunks let totalChunkTime = 0; + for (const [index, chunk] of chunks.rows.entries()) { const chunkStart = performance.now(); @@ -117,6 +121,7 @@ describe('Retrieval Augmented Generation (RAG)', () => { ); const chunkEnd = performance.now(); + totalChunkTime += chunkEnd - chunkStart; } @@ -179,6 +184,7 @@ Answer:`, } else if (log.startsWith('[POSTGRES]')) { acc.postgres += parseFloat(log.split(': ')[1]); } + return acc; }, { ollama: 0, postgres: 0 }); diff --git a/sandbox/ollama/scripts/setup-ollama.ts b/sandbox/ollama/scripts/setup-ollama.ts index 753341944..f6c1a1bfe 100644 --- a/sandbox/ollama/scripts/setup-ollama.ts +++ b/sandbox/ollama/scripts/setup-ollama.ts @@ -39,9 +39,11 @@ interface ChatMessage { async function safeApiCall(url: string, options?: RequestInit): Promise { try { const response = await fetch(url, options); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } + return await response.json(); } catch (error) { console.error('API Error:', error); @@ -68,8 +70,10 @@ async function pullModel(modelName: string) { for (const line of lines) { try { const data = JSON.parse(line) as PullResponse; + if (data.status === 'pulling') { const progress = data.total ? Math.round((data.completed || 0) / data.total * 100) : 0; + process.stdout.write(`\rPulling ${modelName}: ${progress}% complete`); } else if (data.status === 'success') { console.log('\nModel pulled successfully!'); @@ -87,7 +91,9 @@ async function pullModel(modelName: string) { async function listModels() { try { const data = await safeApiCall(`${OLLAMA_API}/api/tags`); + console.log('Available models:', data.models); + return data.models; } catch (error) { console.error('Failed to list models:', error); @@ -123,6 +129,7 @@ async function testModel(modelName: string) { for (const line of lines) { try { const data = JSON.parse(line) as { message: ChatMessage }; + if (data.message?.content) { fullResponse += data.message.content; process.stdout.write(data.message.content); diff --git a/sandbox/ollama/src/services/rag.service.ts b/sandbox/ollama/src/services/rag.service.ts index 5466ab74d..cb123623d 100644 --- a/sandbox/ollama/src/services/rag.service.ts +++ b/sandbox/ollama/src/services/rag.service.ts @@ -1,4 +1,5 @@ import { Pool } from 'pg'; + import { OllamaClient } from '../utils/ollama'; interface Document { @@ -52,6 +53,7 @@ export class RAGService { for (const chunk of chunks.rows) { const chunkEmbedding = await this.ollama.generateEmbedding(chunk.content); + await this.pool.query( 'UPDATE intelligence.chunks SET embedding = $1 WHERE id = $2', [chunkEmbedding, chunk.id] diff --git a/sandbox/ollama/src/utils/ollama.ts b/sandbox/ollama/src/utils/ollama.ts index a2b11b115..6fa05e390 100644 --- a/sandbox/ollama/src/utils/ollama.ts +++ b/sandbox/ollama/src/utils/ollama.ts @@ -35,6 +35,7 @@ export class OllamaClient { } const data: OllamaEmbeddingResponse = await response.json(); + return data.embedding; } @@ -60,6 +61,7 @@ export class OllamaClient { } const data: OllamaResponse = await response.json(); + return data.response; } @@ -89,13 +91,16 @@ export class OllamaClient { } const reader = response.body?.getReader(); + if (!reader) { throw new Error('Failed to get response reader'); } const decoder = new TextDecoder(); + while (true) { const { done, value } = await reader.read(); + if (done) break; const chunk = decoder.decode(value); @@ -104,6 +109,7 @@ export class OllamaClient { for (const line of lines) { try { const data: OllamaResponse = JSON.parse(line); + if (data.response) { onChunk(data.response); } diff --git a/streaming/content-type-stream/__tests__/mimetypes.test.ts b/streaming/content-type-stream/__tests__/mimetypes.test.ts index 45882e2c0..f5aa312c9 100644 --- a/streaming/content-type-stream/__tests__/mimetypes.test.ts +++ b/streaming/content-type-stream/__tests__/mimetypes.test.ts @@ -6,10 +6,11 @@ import { basename } from 'path'; import { streamContentType } from '../src'; const files = [] - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**')) - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**/.*')) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**`)) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**/.*`)) .filter((file) => { const key = file.split('kitchen-sink')[1]; + return key != ''; }) .map((f) => ({ @@ -17,9 +18,10 @@ const files = [] path: f })); -const malicious = glob(__dirname + '/../../../__fixtures__/malicious/**') +const malicious = glob(`${__dirname }/../../../__fixtures__/malicious/**`) .filter((file) => { const key = file.split('malicious')[1]; + return key != ''; }) .map((f) => ({ @@ -42,6 +44,7 @@ describe('mimetypes', () => { filename: file.path } ); + res[key] = { magic, contentType @@ -63,6 +66,7 @@ describe('mimetypes', () => { filename: file.path } ); + res[key] = { magic, contentType diff --git a/streaming/content-type-stream/src/content-stream.ts b/streaming/content-type-stream/src/content-stream.ts index ddaf2ea87..2717aca56 100644 --- a/streaming/content-type-stream/src/content-stream.ts +++ b/streaming/content-type-stream/src/content-stream.ts @@ -11,6 +11,7 @@ export class ContentStream extends stream.Transform { this.etagsum = etag.createHash(); this.uuidsum = uuid.createHash(); } + _write(chunk, enc, next) { this.shasum.update(chunk); this.etagsum.update(chunk); @@ -18,6 +19,7 @@ export class ContentStream extends stream.Transform { this.push(chunk); next(); } + _flush(done) { this.emit('contents', { uuid: this.uuidsum.digest(), diff --git a/streaming/etag-hash/__tests__/etag-hash.test.ts b/streaming/etag-hash/__tests__/etag-hash.test.ts index 83717fade..c94b8c13b 100644 --- a/streaming/etag-hash/__tests__/etag-hash.test.ts +++ b/streaming/etag-hash/__tests__/etag-hash.test.ts @@ -7,6 +7,7 @@ describe('ETag', () => { for (let i = 0; i < strings.length; i++) { const str = strings[i]; + res[str] = createHash() .update(Buffer.from(str, 'utf8')) .digest(); @@ -19,11 +20,13 @@ describe('ETag', () => { const hash = createHash(); const SZ = 5 * 1024 * 1024; + for (let i = 0; i < SZ; i++) { hash.update(Buffer.from(String(i), 'utf8')); } const res = hash.digest(); + expect(res).toMatchSnapshot(); }); }); diff --git a/streaming/etag-hash/src/index.ts b/streaming/etag-hash/src/index.ts index 44047a346..e68476a03 100644 --- a/streaming/etag-hash/src/index.ts +++ b/streaming/etag-hash/src/index.ts @@ -21,6 +21,7 @@ export class ETagHash { this.bytes += len; } else { const bytesNeeded = this.partSizeInBytes - this.bytes; + this.sums[this.part].update(chunk.subarray(0, bytesNeeded)); this.part++; this.sums.push(createCryptoHash('md5')); diff --git a/streaming/etag-stream/__tests__/etag-stream.test.ts b/streaming/etag-stream/__tests__/etag-stream.test.ts index 10ab69486..35462c2a3 100644 --- a/streaming/etag-stream/__tests__/etag-stream.test.ts +++ b/streaming/etag-stream/__tests__/etag-stream.test.ts @@ -7,6 +7,7 @@ import ETagStream from '../src'; const getETag = (stream: NodeJS.ReadableStream): Promise => { return new Promise((resolve, reject) => { let result: string | null = null; + stream .on('error', (e) => reject(e)) .on('data', (data: Buffer) => { @@ -46,6 +47,7 @@ describe('etags', () => { const file = files[i]; const key = file.split('__fixtures__')[1]; const stream = new ETagStream({ mode: 'through' }); + res[key] = await getETagThrough(createReadStream(file).pipe(stream)); } @@ -60,6 +62,7 @@ describe('etags', () => { const file = files[i]; const key = file.split('__fixtures__')[1]; const stream = new ETagStream({ mode: 'buffer' }); // changed `false` to a valid type + res[key] = await getETag(createReadStream(file).pipe(stream)); } diff --git a/streaming/etag-stream/src/index.ts b/streaming/etag-stream/src/index.ts index 7c79637cf..311e66060 100644 --- a/streaming/etag-stream/src/index.ts +++ b/streaming/etag-stream/src/index.ts @@ -12,6 +12,7 @@ class ETagStream extends Transform { constructor(opts: ETagStreamOptions = {}) { const { partSizeInMb = 5, mode = 'through' } = opts; + super(); this.mode = mode; this.hash = createHash(partSizeInMb); @@ -27,6 +28,7 @@ class ETagStream extends Transform { _flush(callback: TransformCallback): void { const digest = this.hash.digest(); + if (this.mode === 'through') { this.emit('etag', digest); } else { diff --git a/streaming/mime-bytes/__tests__/cad-file-detection.test.ts b/streaming/mime-bytes/__tests__/cad-file-detection.test.ts index ff693e5fb..775bea3f2 100644 --- a/streaming/mime-bytes/__tests__/cad-file-detection.test.ts +++ b/streaming/mime-bytes/__tests__/cad-file-detection.test.ts @@ -53,6 +53,7 @@ describe('CAD File Detection', () => { it('should use content type mapping for DWG files', () => { const contentType = getContentTypeForExtension('dwg', 'binary'); + expect(contentType).toBe('application/acad'); }); @@ -83,6 +84,7 @@ describe('CAD File Detection', () => { it('should use content type mapping for EMF files', () => { const contentType = getContentTypeForExtension('emf', 'binary'); + expect(contentType).toBe('image/emf'); }); }); @@ -90,6 +92,7 @@ describe('CAD File Detection', () => { describe('DXF file detection', () => { it('should use content type mapping for DXF files', () => { const contentType = getContentTypeForExtension('dxf', 'ascii'); + expect(contentType).toBe('image/vnd.dxf'); }); diff --git a/streaming/mime-bytes/__tests__/charset-content-type.test.ts b/streaming/mime-bytes/__tests__/charset-content-type.test.ts index 5b7538654..4d7d0dfdd 100644 --- a/streaming/mime-bytes/__tests__/charset-content-type.test.ts +++ b/streaming/mime-bytes/__tests__/charset-content-type.test.ts @@ -11,21 +11,25 @@ describe('Charset-aware Content Type Detection', () => { describe('getContentTypeForExtension', () => { it('should return text/x-typescript for .ts files with utf-8 charset', () => { const contentType = getContentTypeForExtension('ts', 'utf-8'); + expect(contentType).toBe('text/x-typescript'); }); it('should return video/mp2t for .ts files with binary charset', () => { const contentType = getContentTypeForExtension('ts', 'binary'); + expect(contentType).toBe('video/mp2t'); }); it('should return text/x-typescript for .tsx files with utf-8 charset', () => { const contentType = getContentTypeForExtension('tsx', 'utf-8'); + expect(contentType).toBe('text/x-typescript'); }); it('should return null for .tsx files with binary charset', () => { const contentType = getContentTypeForExtension('tsx', 'binary'); + expect(contentType).toBeNull(); }); @@ -55,18 +59,21 @@ describe('Charset-aware Content Type Detection', () => { `); const result = await detector.detectFromBuffer(tsContent); + expect(result).toBeDefined(); expect(result?.charset).toBe('utf-8'); // Since we don't have magic bytes for TS files, it will be detected as generic text // But the content type lookup should work with the charset const contentType = getContentTypeForExtension('ts', result?.charset); + expect(contentType).toBe('text/x-typescript'); }); it('should detect binary MPEG-TS file as video/mp2t', async () => { // MPEG-TS sync byte pattern (0x47 every 188 bytes) const mpegTsContent = Buffer.alloc(376); + mpegTsContent[0] = 0x47; mpegTsContent[188] = 0x47; // Add some binary data @@ -75,9 +82,11 @@ describe('Charset-aware Content Type Detection', () => { } const result = await detector.detectFromBuffer(mpegTsContent); + expect(result?.charset).toBe('binary'); const contentType = getContentTypeForExtension('ts', result?.charset); + expect(contentType).toBe('video/mp2t'); }); }); @@ -123,10 +132,12 @@ describe('Charset-aware Content Type Detection', () => { `); const result = await detector.detectFromBuffer(svgContent); + expect(result).toBeDefined(); // SVG files might be detected as XML or text const contentType = getContentTypeForExtension('svg'); + expect(contentType).toBe('image/svg+xml'); }); }); diff --git a/streaming/mime-bytes/__tests__/file-type-detector.test.ts b/streaming/mime-bytes/__tests__/file-type-detector.test.ts index f083614f5..49d310419 100644 --- a/streaming/mime-bytes/__tests__/file-type-detector.test.ts +++ b/streaming/mime-bytes/__tests__/file-type-detector.test.ts @@ -17,6 +17,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(pngBuffer); + expect(result).toBeDefined(); expect(result?.name).toBe('png'); expect(result?.mimeType).toBe('image/png'); @@ -31,6 +32,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(jpegBuffer); + expect(result).toBeDefined(); expect(result?.name).toBe('jpeg'); expect(result?.mimeType).toBe('image/jpeg'); @@ -45,6 +47,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(zipBuffer); + expect(result).toBeDefined(); expect(result?.name).toBe('zip'); expect(result?.mimeType).toBe('application/zip'); @@ -58,6 +61,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(pdfBuffer); + expect(result).toBeDefined(); expect(result?.name).toBe('pdf'); expect(result?.mimeType).toBe('application/pdf'); @@ -71,6 +75,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(mp4Buffer); + expect(result).toBeDefined(); expect(result?.name).toBe('mp4'); expect(result?.mimeType).toBe('video/mp4'); @@ -83,6 +88,7 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(utf8Buffer); + expect(result).toBeDefined(); expect(result?.name).toBe('utf8'); expect(result?.mimeType).toBe('text/plain'); @@ -95,12 +101,14 @@ describe('FileTypeDetector', () => { ]); const result = await detector.detectFromBuffer(unknownBuffer); + expect(result).toBeNull(); }); it('should handle empty buffers', async () => { const emptyBuffer = Buffer.alloc(0); const result = await detector.detectFromBuffer(emptyBuffer); + expect(result).toBeNull(); }); }); @@ -108,6 +116,7 @@ describe('FileTypeDetector', () => { describe('detectFromExtension', () => { it('should detect by extension with dot', () => { const results = detector.detectFromExtension('.png'); + expect(results).toHaveLength(1); expect(results[0].name).toBe('png'); expect(results[0].mimeType).toBe('image/png'); @@ -116,17 +125,20 @@ describe('FileTypeDetector', () => { it('should detect by extension without dot', () => { const results = detector.detectFromExtension('jpg'); + expect(results.length).toBeGreaterThan(0); expect(results[0].extensions).toContain('jpg'); }); it('should return multiple results for ambiguous extensions', () => { const results = detector.detectFromExtension('xml'); + expect(results.length).toBeGreaterThan(0); }); it('should handle unknown extensions', () => { const results = detector.detectFromExtension('xyz123'); + expect(results).toHaveLength(0); }); }); @@ -160,6 +172,7 @@ describe('FileTypeDetector', () => { }); const result = await detector.detectFromStream(errorStream); + expect(result).toBeNull(); }); }); @@ -167,12 +180,14 @@ describe('FileTypeDetector', () => { describe('getByCategory', () => { it('should return file types by category', () => { const imageTypes = detector.getByCategory('image'); + expect(imageTypes.length).toBeGreaterThan(0); expect(imageTypes.every(ft => ft.category === 'image')).toBe(true); }); it('should return empty array for unknown category', () => { const unknownTypes = detector.getByCategory('unknown-category'); + expect(unknownTypes).toHaveLength(0); }); }); @@ -189,6 +204,7 @@ describe('FileTypeDetector', () => { detector.addFileType(customType); const allTypes = detector.getAllFileTypes(); + expect(allTypes).toContainEqual(customType); }); @@ -196,6 +212,7 @@ describe('FileTypeDetector', () => { // Create a new detector instance for this test to avoid affecting other tests const testDetector = new FileTypeDetector(); const removed = testDetector.removeFileType('png'); + expect(removed).toBe(true); const pngBuffer = Buffer.from([ @@ -203,6 +220,7 @@ describe('FileTypeDetector', () => { ]); const result = await testDetector.detectFromBuffer(pngBuffer); + expect(result).toBeNull(); }); }); @@ -221,6 +239,7 @@ describe('FileTypeDetector', () => { describe('getStatistics', () => { it('should return statistics about registered file types', () => { const stats = detector.getStatistics(); + expect(stats.totalTypes).toBeGreaterThan(0); expect(stats.byCategory).toBeDefined(); expect(stats.byMimePrefix).toBeDefined(); @@ -259,18 +278,21 @@ describe('FileTypeDetector', () => { it('should detect UTF-8 with BOM', async () => { const utf8BOM = Buffer.from([0xEF, 0xBB, 0xBF, 0x48, 0x65, 0x6C, 0x6C, 0x6F]); // BOM + "Hello" const result = await detector.detectFromBuffer(utf8BOM); + expect(result?.charset).toBe('utf-8'); }); it('should detect UTF-8 without BOM', async () => { const utf8Text = Buffer.from('Hello, δΈ–η•Œ! 🌍', 'utf-8'); const result = await detector.detectFromBuffer(utf8Text); + expect(result?.charset).toBe('utf-8'); }); it('should return null for binary content', async () => { const binaryData = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00]); const result = await detector.detectFromBuffer(binaryData); + expect(result).toBeNull(); // Binary content with no magic bytes returns null }); @@ -278,18 +300,21 @@ describe('FileTypeDetector', () => { // Create invalid UTF-8 sequence const invalidUtf8 = Buffer.from([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0xFE]); // "Hello" + invalid bytes const result = await detector.detectFromBuffer(invalidUtf8); + expect(result?.charset).toBe('ascii'); }); it('should detect UTF-16 LE with BOM', async () => { const utf16LE = Buffer.from([0xFF, 0xFE, 0x48, 0x00, 0x65, 0x00]); // BOM + "He" const result = await detector.detectFromBuffer(utf16LE); + expect(result?.charset).toBe('utf-16le'); }); it('should detect UTF-16 BE with BOM', async () => { const utf16BE = Buffer.from([0xFE, 0xFF, 0x00, 0x48, 0x00, 0x65]); // BOM + "He" const result = await detector.detectFromBuffer(utf16BE); + expect(result?.charset).toBe('utf-16be'); }); }); diff --git a/streaming/mime-bytes/__tests__/fixtures/kitchen-sink.test.ts b/streaming/mime-bytes/__tests__/fixtures/kitchen-sink.test.ts index f1a6b2491..6791443f7 100644 --- a/streaming/mime-bytes/__tests__/fixtures/kitchen-sink.test.ts +++ b/streaming/mime-bytes/__tests__/fixtures/kitchen-sink.test.ts @@ -69,6 +69,7 @@ describe('Kitchen Sink Fixtures', () => { const largeExtensions = ['pdf', 'zip', 'mp4', 'avi', 'mov', 'mkv', 'tar', 'gz']; const largeFiles = files.filter(file => { const ext = path.extname(file).toLowerCase().replace('.', ''); + return largeExtensions.includes(ext); }); diff --git a/streaming/mime-bytes/__tests__/fixtures/malicious.test.ts b/streaming/mime-bytes/__tests__/fixtures/malicious.test.ts index d1733751c..2279b9cd0 100644 --- a/streaming/mime-bytes/__tests__/fixtures/malicious.test.ts +++ b/streaming/mime-bytes/__tests__/fixtures/malicious.test.ts @@ -101,6 +101,7 @@ describe('Malicious Fixtures', () => { // All files should be handled without throwing unhandled errors const totalFiles = Object.keys(errorHandlingResults).length; const handledFiles = Object.values(errorHandlingResults).filter(r => r.status === 'success').length; + expect(handledFiles).toBe(totalFiles); }); @@ -165,6 +166,7 @@ describe('Malicious Fixtures', () => { ]); const gifarResult = await detector.detectFromBuffer(gifarData); + expect(gifarResult).toBeDefined(); expect(gifarResult?.name).toBe('gif89a'); // Should detect as GIF (first valid signature) @@ -183,6 +185,7 @@ describe('Malicious Fixtures', () => { ]); const pdfzipResult = await detector.detectFromBuffer(pdfzipData); + expect(pdfzipResult).toBeDefined(); expect(pdfzipResult?.name).toBe('pdf'); // Should detect as PDF (first valid signature) diff --git a/streaming/mime-bytes/__tests__/integration/typescript-detection.test.ts b/streaming/mime-bytes/__tests__/integration/typescript-detection.test.ts index 6acc02615..087cd8717 100644 --- a/streaming/mime-bytes/__tests__/integration/typescript-detection.test.ts +++ b/streaming/mime-bytes/__tests__/integration/typescript-detection.test.ts @@ -74,11 +74,13 @@ export class UserController { // Test 1: TypeScript content const tsCode = Buffer.from('const x: number = 42;'); const tsResult = await detector.detectWithFallback(tsCode, 'file.ts'); + expect(tsResult?.contentType).toBe('text/x-typescript'); // Test 2: Binary content with .ts extension const binaryContent = Buffer.from([0x47, 0x00, 0x00, 0x00]); const binaryResult = await detector.detectWithFallback(binaryContent, 'video.ts'); + expect(binaryResult?.contentType).toBe('video/mp2t'); }); diff --git a/streaming/mime-bytes/examples/basic-usage.ts b/streaming/mime-bytes/examples/basic-usage.ts index a18f5e752..a931371e3 100644 --- a/streaming/mime-bytes/examples/basic-usage.ts +++ b/streaming/mime-bytes/examples/basic-usage.ts @@ -14,6 +14,7 @@ async function main() { ]); const pngResult = await detector.detectFromBuffer(pngBuffer); + console.log('PNG Detection:', pngResult); // Example 2: Detect from a stream (memory efficient) @@ -25,16 +26,19 @@ async function main() { const stream = Readable.from([jpegData]); const jpegResult = await detector.detectFromStream(stream); + console.log('JPEG Detection:', jpegResult); // Example 3: Detect by extension console.log('\n=== Example 3: Detect by Extension ==='); const extensionResults = detector.detectFromExtension('.pdf'); + console.log('PDF Extension Detection:', extensionResults); // Example 4: Get file types by category console.log('\n=== Example 4: Get by Category ==='); const imageTypes = detector.getByCategory('image'); + console.log(`Found ${imageTypes.length} image types`); console.log('First 5 image types:', imageTypes.slice(0, 5).map(t => t.name)); @@ -43,18 +47,21 @@ async function main() { const unknownData = Buffer.from([0x00, 0x11, 0x22, 0x33]); const unknownStream = Readable.from([unknownData]); const fallbackResult = await detector.detectWithFallback(unknownStream, 'document.docx'); + console.log('Fallback Detection:', fallbackResult); // Example 6: Check specific file type console.log('\n=== Example 6: Check Specific Type ==='); const isPng = detector.isFileType(pngBuffer, 'png'); const isJpeg = detector.isFileType(pngBuffer, 'jpeg'); + console.log('Is PNG?', isPng); console.log('Is JPEG?', isJpeg); // Example 7: Get statistics console.log('\n=== Example 7: Statistics ==='); const stats = detector.getStatistics(); + console.log('Total registered types:', stats.totalTypes); console.log('Types by category:', stats.byCategory); console.log('Types by MIME prefix:', stats.byMimePrefix); diff --git a/streaming/mime-bytes/examples/usage.ts b/streaming/mime-bytes/examples/usage.ts index d6440014a..decee8e77 100644 --- a/streaming/mime-bytes/examples/usage.ts +++ b/streaming/mime-bytes/examples/usage.ts @@ -16,6 +16,7 @@ async function demonstrateUsage() { try { const pngResult = await detector.detectFromStream(pngStream); + console.log('PNG detection:', pngResult); } catch (error) { console.error('Error detecting PNG:', error); @@ -27,6 +28,7 @@ async function demonstrateUsage() { const jpgStream = createReadStream(jpgPath); const jpgResult = await detectFromStream(jpgStream); + console.log('JPEG detection:', jpgResult); // Example 3: Detect with fallback to extension @@ -35,33 +37,39 @@ async function demonstrateUsage() { const textStream = createReadStream(textPath); const textResult = await detector.detectWithFallback(textStream, 'document.txt'); + console.log('Text file detection with fallback:', textResult); // Example 4: Detect from buffer (for already-read data) console.log('\n4. Detect from buffer:'); const pdfMagicBytes = Buffer.from([0x25, 0x50, 0x44, 0x46]); // %PDF const pdfResult = await detector.detectFromBuffer(pdfMagicBytes); + console.log('PDF detection from buffer:', pdfResult); // Example 5: Extension-based detection console.log('\n5. Extension-based detection:'); const extensionResults = detector.detectFromExtension('mp4'); + console.log('MP4 extension detection:', extensionResults); // Example 6: Get file types by category console.log('\n6. Get file types by category:'); const imageTypes = detector.getByCategory('image'); + console.log(`Found ${imageTypes.length} image types:`, imageTypes.slice(0, 5).map(ft => ft.name)); // Example 7: Check if buffer is a specific file type console.log('\n7. Check specific file type:'); const gifBuffer = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); // GIF89a const isGif = detector.isFileType(gifBuffer, 'gif89a'); + console.log('Is GIF89a?', isGif); // Example 8: Get statistics console.log('\n8. File type statistics:'); const stats = detector.getStatistics(); + console.log('Statistics:', stats); // Example 9: Add custom file type @@ -78,6 +86,7 @@ async function demonstrateUsage() { const customBuffer = Buffer.from([0xCA, 0xFE, 0xBA, 0xBE]); const customResult = await detector.detectFromBuffer(customBuffer); + console.log('Custom format detection:', customResult); // Example 10: Handle large files efficiently diff --git a/streaming/mime-bytes/src/file-type-detector.ts b/streaming/mime-bytes/src/file-type-detector.ts index e92387a23..450e4e042 100644 --- a/streaming/mime-bytes/src/file-type-detector.ts +++ b/streaming/mime-bytes/src/file-type-detector.ts @@ -65,6 +65,7 @@ export class FileTypeDetector { if (process.env.NODE_ENV !== 'test') { console.error('Error detecting file type from stream:', error); } + return null; } } @@ -86,6 +87,7 @@ export class FileTypeDetector { for (const offset of offsets) { const fileType = this.checkMagicBytesAtOffset(buffer, offset); + if (fileType) { return this.enhanceDetectionResult(fileType, buffer); } @@ -93,6 +95,7 @@ export class FileTypeDetector { // No magic bytes matched, but we can still detect charset for unknown files const charset = detectCharset(buffer); + if (charset !== 'binary') { // Return a generic text file result return { @@ -119,6 +122,7 @@ export class FileTypeDetector { // Check cache first if (this.extensionCache.has(cleanExt)) { const cachedTypes = this.extensionCache.get(cleanExt)!; + return cachedTypes.map(fileType => ({ name: fileType.name, mimeType: resolveMimeAlias(fileType.mimeType), @@ -169,12 +173,15 @@ export class FileTypeDetector { */ removeFileType(name: string): boolean { const index = this.fileTypes.findIndex(ft => ft.name === name); + if (index !== -1) { this.fileTypes.splice(index, 1); // Clear caches when file types change this.clearCache(); + return true; } + return false; } @@ -222,9 +229,11 @@ export class FileTypeDetector { */ private generateOffsets(bufferLength: number): number[] { const offsets: number[] = []; + for (let i = 0; i <= this.options.maxOffset && i < bufferLength; i += 4) { offsets.push(i); } + return offsets; } @@ -249,6 +258,7 @@ export class FileTypeDetector { } else if (!fileType.contentType) { // Fall back to regular content type lookup if no charset-specific match const inferredContentType = getContentTypeByExtension(primaryExt); + if (inferredContentType) { contentType = inferredContentType; } @@ -281,6 +291,7 @@ export class FileTypeDetector { buffer = input; } else { const peekResult = await peek.promise(input, this.options.peekBytes); + buffer = peekResult[0]; peekStream = peekResult[1]; } @@ -292,12 +303,14 @@ export class FileTypeDetector { // If we have a filename, try to enhance with more specific content type if (filename) { const lastDot = filename.lastIndexOf('.'); + if (lastDot !== -1) { const extension = filename.substring(lastDot + 1); // Check for generic text files that might have specific content types if (magicResult.mimeType === 'text/plain' && magicResult.charset) { const contentType = getContentTypeForExtension(extension, magicResult.charset); + if (contentType) { // Enhance the result with charset-aware content type const enhancedResult = { @@ -305,6 +318,7 @@ export class FileTypeDetector { contentType, confidence: 0.8 // Higher confidence since we have both magic bytes and extension }; + return peekStream ? { ...enhancedResult, _stream: peekStream } : enhancedResult; } } @@ -312,6 +326,7 @@ export class FileTypeDetector { // Check for ZIP files that might be Office Open XML or other specific formats if (magicResult.name === 'zip' || magicResult.mimeType === 'application/zip') { const contentType = getContentTypeForExtension(extension, magicResult.charset || 'binary'); + if (contentType && contentType !== 'application/zip') { // Enhance the result with more specific content type const enhancedResult = { @@ -319,6 +334,7 @@ export class FileTypeDetector { contentType, confidence: 0.9 // High confidence for known ZIP-based formats }; + return peekStream ? { ...enhancedResult, _stream: peekStream } : enhancedResult; } } @@ -332,6 +348,7 @@ export class FileTypeDetector { // Fallback to extension if filename provided if (filename) { const lastDot = filename.lastIndexOf('.'); + if (lastDot !== -1) { const extension = filename.substring(lastDot + 1); const charset = detectCharset(buffer); @@ -355,6 +372,7 @@ export class FileTypeDetector { // Fall back to regular extension detection const extensionResults = this.detectFromExtension(extension); + if (extensionResults.length > 0) { const result = { ...extensionResults[0], @@ -364,6 +382,7 @@ export class FileTypeDetector { // Update content type if charset-specific mapping exists const charsetContentType = getContentTypeForExtension(extension, charset); + if (charsetContentType) { result.contentType = charsetContentType; } @@ -380,6 +399,7 @@ export class FileTypeDetector { if (process.env.NODE_ENV !== 'test') { console.error('Error in detectWithFallback:', error); } + return null; } } @@ -392,9 +412,11 @@ export class FileTypeDetector { */ isFileType(buffer: Buffer, fileTypeName: string): boolean { const fileType = this.fileTypes.find(ft => ft.name === fileTypeName); + if (!fileType) return false; const offset = fileType.offset || 0; + return compareBytes(buffer, fileType.magicBytes, offset); } @@ -416,10 +438,12 @@ export class FileTypeDetector { for (const fileType of this.fileTypes) { // Count by category const category = fileType.category || 'other'; + stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; // Count by MIME prefix const mimePrefix = fileType.mimeType.split('/')[0]; + stats.byMimePrefix[mimePrefix] = (stats.byMimePrefix[mimePrefix] || 0) + 1; } diff --git a/streaming/mime-bytes/src/file-types-registry.ts b/streaming/mime-bytes/src/file-types-registry.ts index ebd92fbce..5fd78b357 100644 --- a/streaming/mime-bytes/src/file-types-registry.ts +++ b/streaming/mime-bytes/src/file-types-registry.ts @@ -1250,6 +1250,7 @@ export function getFileTypeByMagicBytes(magicBytes: string, offset: number = 0): export function getFileTypeByExtension(extension: string): FileTypeDefinition[] { const cleanExt = extension.toLowerCase().replace(/^\./, ''); + return FILE_TYPES.filter(type => type.extensions.some(ext => ext.toLowerCase() === cleanExt) ); @@ -1266,6 +1267,7 @@ function matchSignatureWithWildcards(pattern: string, input: string): boolean { // Handle wildcards (?) in the pattern const regexPattern = pattern.replace(/\?/g, '[0-9a-f]{2}'); const regex = new RegExp(`^${regexPattern}`, 'i'); + return regex.test(input); } @@ -1300,6 +1302,7 @@ export function detectCharset(buffer: Buffer): string { for (let i = 0; i < sample.length; i++) { const byte = sample[i]; + if (byte === 0) nullCount++; // Count control characters except tab, newline, carriage return if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { @@ -1319,6 +1322,7 @@ export function detectCharset(buffer: Buffer): string { try { // Use TextDecoder with fatal flag to validate UTF-8 new TextDecoder('utf-8', { fatal: true }).decode(sample); + return 'utf-8'; } catch { // If UTF-8 decoding fails, it might be a legacy encoding @@ -1336,6 +1340,7 @@ export function getContentTypeForExtension(extension: string, charset?: string): const exactMatch = CONTENT_TYPE_MAPPINGS.find( mapping => mapping[0].includes(ext) && mapping[2] === charset ); + if (exactMatch) { return exactMatch[1]; } diff --git a/streaming/mime-bytes/src/peak.ts b/streaming/mime-bytes/src/peak.ts index cb0a19e58..2146f69af 100644 --- a/streaming/mime-bytes/src/peak.ts +++ b/streaming/mime-bytes/src/peak.ts @@ -26,11 +26,13 @@ export class BufferPeekStream extends Transform { // After peeking, just pass through this.push(chunk); callback(); + return; } // Accumulate data until we have enough to peek const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); + this.buffer = Buffer.concat([this.buffer, chunkBuffer]); this.bufferLength += chunkBuffer.length; @@ -40,6 +42,7 @@ export class BufferPeekStream extends Transform { // Emit the peek event with the requested bytes const peekBuffer = this.buffer.slice(0, this.peekBytes); + this.emit('peek', peekBuffer); // Push all accumulated data diff --git a/streaming/mime-bytes/src/utils/extensions.ts b/streaming/mime-bytes/src/utils/extensions.ts index e5cc9b04d..fbd153260 100644 --- a/streaming/mime-bytes/src/utils/extensions.ts +++ b/streaming/mime-bytes/src/utils/extensions.ts @@ -8,9 +8,11 @@ export function normalizeExtension(extension: string): string { // Extract extension from filename export function getExtension(filename: string): string { const lastDot = filename.lastIndexOf('.'); + if (lastDot === -1 || lastDot === filename.length - 1) { return ''; } + return normalizeExtension(filename.substring(lastDot + 1)); } @@ -22,6 +24,7 @@ export function isCompressedExtension(extension: string): boolean { 'iso', 'lha', 'lzh', 'pkg', 'deb', 'rpm', 'msi', 'jar', 'war', 'ear', 'sar', 'aar', 'apk', 'ipa', 'xpi', 'egg', 'whl', 'gem' ]; + return compressed.includes(normalizeExtension(extension)); } @@ -32,6 +35,7 @@ export function isDocumentExtension(extension: string): boolean { 'xls', 'xlsx', 'ods', 'csv', 'ppt', 'pptx', 'odp', 'epub', 'mobi', 'azw', 'azw3', 'fb2', 'lit', 'pdb', 'ps', 'eps', 'indd', 'xps' ]; + return documents.includes(normalizeExtension(extension)); } @@ -47,6 +51,7 @@ export function isMediaExtension(extension: string): boolean { 'ape', 'wv', 'amr', 'ac3', 'dts', 'spx', 'mid', 'midi', 'kar', 'aiff', 'aif', 'aifc', 'au', 'snd', 'voc', 'ra', 'rm', 'ram' ]; + return media.includes(normalizeExtension(extension)); } @@ -62,6 +67,7 @@ export function isImageExtension(extension: string): boolean { 'drf', 'k25', 'kdc', 'mdc', 'mef', 'mos', 'mrw', 'pef', 'ptx', 'pxn', 'r3d', 'x3f', 'qoi' ]; + return images.includes(normalizeExtension(extension)); } @@ -74,6 +80,7 @@ export function isExecutableExtension(extension: string): boolean { 'cpl', 'ocx', 'sys', 'drv', 'efi', 'mui', 'ax', 'ime', 'rs', 'tsp', 'fon', 'wasm', 'ko', 'mod', 'prx', 'puff', 'axf', 'dex' ]; + return executables.includes(normalizeExtension(extension)); } @@ -116,7 +123,7 @@ export function getDoubleExtension(filename: string): string | null { const lower = filename.toLowerCase(); for (const doubleExt of DOUBLE_EXTENSIONS) { - if (lower.endsWith('.' + doubleExt)) { + if (lower.endsWith(`.${ doubleExt}`)) { return doubleExt; } } diff --git a/streaming/mime-bytes/src/utils/magic-bytes.ts b/streaming/mime-bytes/src/utils/magic-bytes.ts index 5615d6efc..a515dcf8f 100644 --- a/streaming/mime-bytes/src/utils/magic-bytes.ts +++ b/streaming/mime-bytes/src/utils/magic-bytes.ts @@ -3,8 +3,10 @@ export function hexToBuffer(hexArray: string[]): Buffer { const bytes = hexArray.map(hex => { if (hex === '?') return 0; // Wildcard placeholder + return parseInt(hex.replace(/0x/i, ''), 16); }); + return Buffer.from(bytes); } @@ -39,10 +41,12 @@ export function compareBytes(buffer: Buffer, pattern: string[], offset: number = export function findMagicBytes(buffer: Buffer, patterns: Array<{pattern: string[], offset?: number}>): number { for (let i = 0; i < patterns.length; i++) { const { pattern, offset = 0 } = patterns[i]; + if (compareBytes(buffer, pattern, offset)) { return i; } } + return -1; } @@ -51,6 +55,7 @@ export function extractBytes(buffer: Buffer, offset: number, length: number): Bu if (offset + length > buffer.length) { return buffer.slice(offset); } + return buffer.slice(offset, offset + length); } @@ -61,6 +66,7 @@ export function isTextLike(buffer: Buffer): boolean { for (let i = 0; i < sampleSize; i++) { const byte = buffer[i]; + // Check for printable ASCII characters, tabs, newlines, carriage returns if ((byte >= 32 && byte <= 126) || byte === 9 || byte === 10 || byte === 13) { printableCount++; diff --git a/streaming/mime-bytes/src/utils/mime-types.ts b/streaming/mime-bytes/src/utils/mime-types.ts index 0c89b1285..f1995d2fd 100644 --- a/streaming/mime-bytes/src/utils/mime-types.ts +++ b/streaming/mime-bytes/src/utils/mime-types.ts @@ -15,9 +15,11 @@ export type MimeCategory = typeof MIME_CATEGORIES[keyof typeof MIME_CATEGORIES]; // Extract category from MIME type export function getMimeCategory(mimeType: string): MimeCategory | null { const category = mimeType.split('/')[0]; + if (Object.values(MIME_CATEGORIES).includes(category as MimeCategory)) { return category as MimeCategory; } + return null; } @@ -97,5 +99,6 @@ const MIME_ALIASES: Record = { // Resolve MIME type aliases export function resolveMimeAlias(mimeType: string): string { const normalized = normalizeMimeType(mimeType); + return MIME_ALIASES[normalized] || normalized; } \ No newline at end of file diff --git a/streaming/s3-streamer/__tests__/uploads.test.ts b/streaming/s3-streamer/__tests__/uploads.test.ts index e75b9c64e..87ee76029 100644 --- a/streaming/s3-streamer/__tests__/uploads.test.ts +++ b/streaming/s3-streamer/__tests__/uploads.test.ts @@ -1,6 +1,6 @@ import { S3Client } from '@aws-sdk/client-s3'; -import { getEnvOptions } from '@pgpmjs/env'; import { createS3Bucket } from '@constructive-io/s3-utils'; +import { getEnvOptions } from '@pgpmjs/env'; import { createReadStream } from 'fs'; import { sync as glob } from 'glob'; import { basename } from 'path'; @@ -40,6 +40,7 @@ jest.setTimeout(3000000); beforeAll(async () => { process.env.IS_MINIO = 'true'; // Ensure MinIO behavior in createS3Bucket const result = await createS3Bucket(s3Client, BUCKET_NAME); + if (!result.success) throw new Error('Failed to create test S3 bucket'); }); @@ -51,8 +52,8 @@ afterAll(async () => { const files = [] - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**')) - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**/.*')) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**`)) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**/.*`)) .filter((file) => file.split('kitchen-sink')[1] !== '') .map((f) => ({ key: basename(f), @@ -71,14 +72,16 @@ describe('uploads', () => { try { const res: Record = {}; + for (const file of files) { const key = file.key; const readStream = createReadStream(file.path); const results = await streamer.upload({ readStream, filename: file.path, - key: 'db1/assets/' + basename(file.path) + key: `db1/assets/${ basename(file.path)}` }); + res[key] = results; } @@ -104,6 +107,7 @@ describe('uploads', () => { try { const res: Record = {}; + for (const file of files) { const key = file.key; const readStream = createReadStream(file.path); @@ -112,8 +116,9 @@ describe('uploads', () => { readStream, filename: file.path, bucket: BUCKET_NAME, - key: 'db1/assets/' + basename(file.path) + key: `db1/assets/${ basename(file.path)}` }); + res[key] = results; } diff --git a/streaming/s3-streamer/src/utils.ts b/streaming/s3-streamer/src/utils.ts index e7d43be25..3b517f500 100644 --- a/streaming/s3-streamer/src/utils.ts +++ b/streaming/s3-streamer/src/utils.ts @@ -66,6 +66,7 @@ export const uploadFromStream = ({ Bucket: bucket, Key: key }; + pass.emit('upload', result); }) .catch((err) => { @@ -110,7 +111,7 @@ export const asyncUpload = ({ }; contentStream - .on('contents', function (results: unknown) { + .on('contents', (results: unknown) => { contents = results; tryResolve(); }) diff --git a/streaming/s3-utils/src/policies.ts b/streaming/s3-utils/src/policies.ts index 0df21249b..ee33fc8ce 100644 --- a/streaming/s3-utils/src/policies.ts +++ b/streaming/s3-utils/src/policies.ts @@ -10,11 +10,13 @@ export async function createS3Bucket(client: S3Client, Bucket: string): Promise< } catch (e: any) { if (e.name === 'BucketAlreadyOwnedByYou' || e.Code === 'BucketAlreadyOwnedByYou') { console.warn(`[createS3Bucket] Bucket "${Bucket}" already exists`); + return { success: true }; - } else { + } console.error('[createS3Bucket error - createBucket]', e); + return { success: false }; - } + } // Check if it's MinIO by looking at the endpoint @@ -86,6 +88,7 @@ export async function createS3Bucket(client: S3Client, Bucket: string): Promise< return { success: true }; } catch (e) { console.error('[createS3Bucket error - post-create]', e); + return { success: false }; } } diff --git a/streaming/s3-utils/src/utils.ts b/streaming/s3-utils/src/utils.ts index 434a9ac1b..e424f9583 100644 --- a/streaming/s3-utils/src/utils.ts +++ b/streaming/s3-utils/src/utils.ts @@ -23,6 +23,7 @@ export interface UploadResult { export const fileExists = async ({ client, bucket, key }: FileOperationArgs): Promise => { try { await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); + return true; } catch (e: any) { if (e.name === 'NotFound' || e.$metadata?.httpStatusCode === 404) return false; @@ -87,7 +88,7 @@ export const upload = async ({ ETag: result.ETag, Bucket: bucket, Key: key, - key: key, // v2 had both Key and key + key, // v2 had both Key and key }; }; @@ -117,8 +118,9 @@ export const uploadThrough = ({ ETag: data.ETag, Bucket: bucket, Key: key, - key: key, // v2 had both Key and key + key, // v2 had both Key and key }; + pass.emit('upload', result); }) .catch((err) => { diff --git a/streaming/stream-to-etag/__tests__/stream-to-etag.test.ts b/streaming/stream-to-etag/__tests__/stream-to-etag.test.ts index 028e933c9..0c15ca9bd 100644 --- a/streaming/stream-to-etag/__tests__/stream-to-etag.test.ts +++ b/streaming/stream-to-etag/__tests__/stream-to-etag.test.ts @@ -6,11 +6,12 @@ import stream2etag from '../src'; describe('etags', () => { it('calculates etags properly', async () => { const res: Record = {}; - const files = glob(__dirname + '/../__fixtures__/*.*'); + const files = glob(`${__dirname }/../__fixtures__/*.*`); for (let i = 0; i < files.length; i++) { const file = files[i]; const key = file.split('__fixtures__')[1]; + res[key] = await stream2etag(createReadStream(file)); } diff --git a/streaming/stream-to-etag/src/index.ts b/streaming/stream-to-etag/src/index.ts index b70be6d3b..f90d066e7 100644 --- a/streaming/stream-to-etag/src/index.ts +++ b/streaming/stream-to-etag/src/index.ts @@ -11,6 +11,7 @@ export default function stream2etag(stream: Readable, partSizeInMb: number = 5): }) .on('data', (chunk: Buffer | string) => { const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk; + hash.update(buf); }) .on('end', () => { diff --git a/streaming/upload-names/__tests__/names.test.ts b/streaming/upload-names/__tests__/names.test.ts index 80ed4fd30..941467675 100644 --- a/streaming/upload-names/__tests__/names.test.ts +++ b/streaming/upload-names/__tests__/names.test.ts @@ -6,10 +6,11 @@ import getName from '../src'; jest.setTimeout(3000000); const files = [] - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**')) - .concat(glob(__dirname + '/../../../__fixtures__/kitchen-sink/**/.*')) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**`)) + .concat(glob(`${__dirname }/../../../__fixtures__/kitchen-sink/**/.*`)) .filter((file) => { const key = file.split('kitchen-sink')[1]; + return key != ''; }) .concat([ @@ -36,9 +37,11 @@ describe('uploads', () => { it('english', async () => { const res = {}; const use = files; + for (let i = 0; i < use.length; i++) { const file = use[i]; const results = getName(file); + res[basename(file)] = results; } expect(res).toMatchSnapshot(); @@ -46,9 +49,11 @@ describe('uploads', () => { it('upper', async () => { const res = {}; const use = files; + for (let i = 0; i < use.length; i++) { const file = use[i]; const results = getName(file, { lower: false }); + res[basename(file)] = results; } expect(res).toMatchSnapshot(); @@ -56,8 +61,10 @@ describe('uploads', () => { it('any w english', async () => { const res = {}; const use = allNonEnglish; + for (let i = 0; i < use.length; i++) { const file = use[i]; + try { getName(file); } catch (e) { @@ -68,9 +75,11 @@ describe('uploads', () => { it('any', async () => { const res = {}; const use = allNonEnglish; + for (let i = 0; i < use.length; i++) { const file = use[i]; const results = await getName(file, { english: false }); + res[basename(file)] = results; } expect(res).toMatchSnapshot(); @@ -83,6 +92,7 @@ describe('uploads', () => { 'some..file..name.txt' ]; const res = {}; + for (const input of inputs) { res[input] = getName(input); } @@ -97,6 +107,7 @@ describe('uploads', () => { 'lev.Ρ€', ]; const res = {}; + for (const input of inputs) { res[input] = getName(input); } @@ -112,6 +123,7 @@ describe('uploads', () => { '😊😊😊.txt' ]; const res = {}; + for (const input of inputs) { try { getName(input, { english: true }); @@ -130,6 +142,7 @@ describe('uploads', () => { '😊😊😊.txt' ]; const res = {}; + for (const input of inputs) { res[input] = getName(input, { english: false }); } diff --git a/streaming/upload-names/src/index.ts b/streaming/upload-names/src/index.ts index f42a7484b..f6898dfb9 100644 --- a/streaming/upload-names/src/index.ts +++ b/streaming/upload-names/src/index.ts @@ -1,4 +1,5 @@ import { basename, extname } from 'path'; + import slugify from './slugify'; interface Options { @@ -25,6 +26,7 @@ export default ( // Step 3: Sluggify (ASCII-only if english = true) let slug = name; + if (english) { slug = slugify(name); if (slug.length === 0 && name.length > 0) { @@ -35,5 +37,6 @@ export default ( } const result = english ? `${slug}${slugify(ext)}` : `${name}${ext}`; + return lower ? result.toLowerCase() : result; }; diff --git a/streaming/uuid-hash/__tests__/uuid-hash.test.ts b/streaming/uuid-hash/__tests__/uuid-hash.test.ts index b651da21b..d6f0854c6 100644 --- a/streaming/uuid-hash/__tests__/uuid-hash.test.ts +++ b/streaming/uuid-hash/__tests__/uuid-hash.test.ts @@ -4,8 +4,10 @@ describe('UUID v5', () => { it('uuid v5 hash', async () => { const res: Record = {}; const strings = ['Hello World', 'Another String', 'uuid']; + for (let i = 0; i < strings.length; i++) { const str = strings[i]; + res[str] = createHash() .update(str) .digest(); @@ -16,8 +18,10 @@ describe('UUID v5', () => { it('uuid v5 hash w custom namespace', async () => { const res: Record = {}; const strings = ['Hello World', 'Another String', 'uuid']; + for (let i = 0; i < strings.length; i++) { const str = strings[i]; + res[str] = createHash('e8613ca4-b17d-5979-82ea-86c1373c4ffc') .update(str) .digest(); diff --git a/streaming/uuid-hash/src/index.ts b/streaming/uuid-hash/src/index.ts index 43e072397..cf1e99eac 100644 --- a/streaming/uuid-hash/src/index.ts +++ b/streaming/uuid-hash/src/index.ts @@ -6,31 +6,38 @@ export const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; const convertToBuffer = (bytes: string | number[] | Buffer): Buffer => { if (Array.isArray(bytes)) { return Buffer.from(bytes); - } else if (typeof bytes === 'string') { + } if (typeof bytes === 'string') { return Buffer.from(bytes, 'utf8'); } + return bytes; }; const uuidToBytes = (uuid: string): number[] => { const bytes: number[] = []; + uuid.replace(/[a-fA-F0-9]{2}/g, (hex) => { bytes.push(parseInt(hex, 16)); + return ''; }); + return bytes; }; const hex: string[] = []; + for (let i = 0; i < 256; i++) { hex[i] = (i < 16 ? '0' : '') + i.toString(16); } const hexToBytes = (hexStr: string): number[] => { const bytes: number[] = []; + for (let c = 0; c < hexStr.length; c += 2) { bytes.push(parseInt(hexStr.substr(c, 2), 16)); } + return bytes; }; @@ -57,35 +64,37 @@ export class UuidHash { update(chunk: string | number[] | Buffer): this { this.shasum.update(convertToBuffer(chunk)); + return this; } digest(): string { const r = hexToBytes(this.shasum.digest('hex')); + r[6] = (r[6] & 0x0f) | this.version; r[8] = (r[8] & 0x3f) | 0x80; return ( - hex[r[0]] + + `${hex[r[0]] + hex[r[1]] + hex[r[2]] + - hex[r[3]] + - '-' + - hex[r[4]] + - hex[r[5]] + - '-' + - hex[r[6]] + - hex[r[7]] + - '-' + - hex[r[8]] + - hex[r[9]] + - '-' + - hex[r[10]] + - hex[r[11]] + - hex[r[12]] + - hex[r[13]] + - hex[r[14]] + - hex[r[15]] + hex[r[3]] + }-${ + hex[r[4]] + }${hex[r[5]] + }-${ + hex[r[6]] + }${hex[r[7]] + }-${ + hex[r[8]] + }${hex[r[9]] + }-${ + hex[r[10]] + }${hex[r[11]] + }${hex[r[12]] + }${hex[r[13]] + }${hex[r[14]] + }${hex[r[15]]}` ); } } diff --git a/streaming/uuid-stream/__tests__/uuid-stream.test.ts b/streaming/uuid-stream/__tests__/uuid-stream.test.ts index 24b7b431d..326db24c2 100644 --- a/streaming/uuid-stream/__tests__/uuid-stream.test.ts +++ b/streaming/uuid-stream/__tests__/uuid-stream.test.ts @@ -46,6 +46,7 @@ describe('UUID v5', () => { const str = strings[i]; const s = new StringStream(str); const stream = new UuidStream(); + res[str] = await getUuid(s.pipe(stream)); }