diff --git a/pgpm/core/__tests__/files/plan/writer.test.ts b/pgpm/core/__tests__/files/plan/writer.test.ts new file mode 100644 index 000000000..41b4ade9c --- /dev/null +++ b/pgpm/core/__tests__/files/plan/writer.test.ts @@ -0,0 +1,218 @@ +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'; + +describe('writeSqitchPlan', () => { + let tempDir: string; + let outputDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-writer-test-')); + outputDir = path.join(tempDir, 'output'); + fs.mkdirSync(outputDir, { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + const createTestRows = (): SqitchRow[] => [ + { + deploy: 'schemas/test/schema', + revert: 'schemas/test/schema', + verify: 'schemas/test/schema', + content: 'CREATE SCHEMA test;', + name: 'create_schema', + deps: [] + }, + { + deploy: 'schemas/test/tables/users/table', + revert: 'schemas/test/tables/users/table', + verify: 'schemas/test/tables/users/table', + content: 'CREATE TABLE test.users (id uuid);', + name: 'create_table', + deps: ['schemas/test/schema'] + } + ]; + + it('should write plan file with simple author name', () => { + const rows = createTestRows(); + const opts = { + outdir: outputDir, + name: 'test-module', + author: 'John Doe', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + 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 '); + expect(content).toContain('schemas/test/tables/users/table [schemas/test/schema] 2017-08-11T08:11:51Z John Doe '); + }); + + it('should parse author with email format correctly', () => { + const rows = createTestRows(); + const opts = { + outdir: outputDir, + name: 'test-module', + author: 'Alex Thompson ', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + // Should have only ONE email part, not two + expect(content).toContain('Alex Thompson '); + expect(content).not.toContain('Alex Thompson <'); + expect(content).not.toContain('@5b0c196eeb62'); + + // Verify the format is correct + expect(content).toMatch(/schemas\/test\/schema 2017-08-11T08:11:51Z Alex Thompson # add create_schema/); + }); + + it('should handle author with email and extra spaces', () => { + const rows = createTestRows(); + const opts = { + outdir: outputDir, + name: 'test-module', + author: ' Jane Smith ', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + // Should trim spaces correctly + expect(content).toContain('Jane Smith '); + expect(content).not.toContain(' Jane Smith '); + }); + + it('should use default author when not provided', () => { + const rows = createTestRows(); + const opts = { + outdir: outputDir, + name: 'test-module', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + expect(content).toContain('constructive '); + }); + + it('should handle rows with dependencies correctly', () => { + const rows: SqitchRow[] = [ + { + deploy: 'schemas/test/schema', + content: 'CREATE SCHEMA test;', + name: 'create_schema', + deps: [] + }, + { + deploy: 'schemas/test/tables/users/table', + content: 'CREATE TABLE test.users (id uuid);', + name: 'create_table', + deps: ['schemas/test/schema', 'schemas/test/tables/roles/table'] + } + ]; + + const opts = { + outdir: outputDir, + name: 'test-module', + author: 'Test User ', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + // First row should not have dependencies bracket + expect(content).toMatch(/^schemas\/test\/schema 2017-08-11T08:11:51Z Test User /m); + + // Second row should have dependencies bracket + expect(content).toMatch(/schemas\/test\/tables\/users\/table \[schemas\/test\/schema schemas\/test\/tables\/roles\/table\] 2017-08-11T08:11:51Z Test User /); + }); + + it('should skip duplicate deploy paths', () => { + const rows: SqitchRow[] = [ + { + deploy: 'schemas/test/schema', + content: 'CREATE SCHEMA test;', + name: 'create_schema', + deps: [] + }, + { + deploy: 'schemas/test/schema', // duplicate + content: 'ALTER SCHEMA test;', + name: 'alter_schema', + deps: [] + } + ]; + + const opts = { + outdir: outputDir, + name: 'test-module', + author: 'Test User', + replacer: (str: string) => str.replace('constructive-extension-name', 'test-module') + }; + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + // 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'); + + consoleSpy.mockRestore(); + }); + + it('should apply replacer function to plan content', () => { + const rows = createTestRows(); + const opts = { + outdir: outputDir, + name: 'test-module', + author: 'Test User', + replacer: (str: string) => { + return str + .replace(/constructive-extension-name/g, 'test-module') + .replace(/schemas\/test/g, 'schemas/custom'); + } + }; + + writeSqitchPlan(rows, opts); + + const planPath = path.join(outputDir, 'test-module', 'pgpm.plan'); + const content = fs.readFileSync(planPath, 'utf-8'); + + expect(content).toContain('%project=test-module'); + expect(content).toContain('%uri=test-module'); + expect(content).toContain('schemas/custom/schema'); + expect(content).toContain('schemas/custom/tables/users/table'); + expect(content).not.toContain('constructive-extension-name'); + }); +}); + diff --git a/pgpm/core/src/files/plan/writer.ts b/pgpm/core/src/files/plan/writer.ts index 1ad75e911..47d3b2418 100644 --- a/pgpm/core/src/files/plan/writer.ts +++ b/pgpm/core/src/files/plan/writer.ts @@ -18,8 +18,23 @@ export function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void fs.mkdirSync(dir, { recursive: true }); const date = (): string => '2017-08-11T08:11:51Z'; // stubbed timestamp - const author = opts.author || 'constructive'; - const email = `${author}@5b0c196eeb62`; + + // Parse author string - it might contain email in format "Name " + const authorInput = (opts.author || 'constructive').trim(); + let authorName = authorInput; + let authorEmail = ''; + + // 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(); + authorEmail = emailMatch[2].trim(); + } else { + // No email in author, use default format + authorName = authorInput; + authorEmail = `${authorName}@5b0c196eeb62`; + } const duplicates: Record = {}; @@ -36,9 +51,9 @@ ${rows duplicates[row.deploy] = true; if (row.deps?.length) { - return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${author} <${email}> # add ${row.name}`; + return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${authorName} <${authorEmail}> # add ${row.name}`; } - return `${row.deploy} ${date()} ${author} <${email}> # add ${row.name}`; + return `${row.deploy} ${date()} ${authorName} <${authorEmail}> # add ${row.name}`; }) .join('\n')} `);