Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions pgpm/core/__tests__/files/plan/writer.test.ts
Original file line number Diff line number Diff line change
@@ -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 <John Doe@5b0c196eeb62>');
expect(content).toContain('schemas/test/schema 2017-08-11T08:11:51Z John Doe <John Doe@5b0c196eeb62>');
expect(content).toContain('schemas/test/tables/users/table [schemas/test/schema] 2017-08-11T08:11:51Z John Doe <John Doe@5b0c196eeb62>');
});

it('should parse author with email format correctly', () => {
const rows = createTestRows();
const opts = {
outdir: outputDir,
name: 'test-module',
author: 'Alex Thompson <[email protected]>',
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 <[email protected]>');
expect(content).not.toContain('Alex Thompson <[email protected]> <');
expect(content).not.toContain('@5b0c196eeb62');

// Verify the format is correct
expect(content).toMatch(/schemas\/test\/schema 2017-08-11T08:11:51Z Alex Thompson <[email protected]> # 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 <[email protected]> ',
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 <[email protected]>');
expect(content).not.toContain(' Jane Smith <[email protected]> ');
});

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 <constructive@5b0c196eeb62>');
});

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 <[email protected]>',
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 <[email protected]>/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 <[email protected]>/);
});

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');
});
});

23 changes: 19 additions & 4 deletions pgpm/core/src/files/plan/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <email>"
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 <email>"
authorName = emailMatch[1].trim();
authorEmail = emailMatch[2].trim();
} else {
// No email in author, use default format
authorName = authorInput;
authorEmail = `${authorName}@5b0c196eeb62`;
}

const duplicates: Record<string, boolean> = {};

Expand All @@ -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')}
`);
Expand Down