schematic-pg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1019 -0
- package/dist/api/auth/errors.d.ts +6 -0
- package/dist/api/auth/errors.js +12 -0
- package/dist/api/auth/jwt-resolver.d.ts +7 -0
- package/dist/api/auth/jwt-resolver.js +59 -0
- package/dist/api/auth/middleware.d.ts +4 -0
- package/dist/api/auth/middleware.js +10 -0
- package/dist/api/auth/policy.d.ts +7 -0
- package/dist/api/auth/policy.js +95 -0
- package/dist/api/auth/template.d.ts +2 -0
- package/dist/api/auth/template.js +24 -0
- package/dist/api/auth/types.d.ts +12 -0
- package/dist/api/auth/types.js +1 -0
- package/dist/api/middleware/db.d.ts +8 -0
- package/dist/api/middleware/db.js +12 -0
- package/dist/api/middleware/errors.d.ts +5 -0
- package/dist/api/middleware/errors.js +27 -0
- package/dist/api/middleware/validate.d.ts +23 -0
- package/dist/api/middleware/validate.js +13 -0
- package/dist/api/types.d.ts +8 -0
- package/dist/api/types.js +1 -0
- package/dist/api/utils/route-naming.d.ts +3 -0
- package/dist/api/utils/route-naming.js +25 -0
- package/dist/api-generator/app-generator.d.ts +11 -0
- package/dist/api-generator/app-generator.js +79 -0
- package/dist/api-generator/custom-route-scanner.d.ts +6 -0
- package/dist/api-generator/custom-route-scanner.js +42 -0
- package/dist/api-generator/generate-api-cli.d.ts +1 -0
- package/dist/api-generator/generate-api-cli.js +28 -0
- package/dist/api-generator/index.d.ts +11 -0
- package/dist/api-generator/index.js +15 -0
- package/dist/api-generator/policy-generator.d.ts +9 -0
- package/dist/api-generator/policy-generator.js +33 -0
- package/dist/api-generator/route-generator.d.ts +20 -0
- package/dist/api-generator/route-generator.js +198 -0
- package/dist/api-generator/utils/policy.d.ts +18 -0
- package/dist/api-generator/utils/policy.js +72 -0
- package/dist/api-generator/zod-schema-generator.d.ts +16 -0
- package/dist/api-generator/zod-schema-generator.js +145 -0
- package/dist/cli/db.d.ts +4 -0
- package/dist/cli/db.js +144 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +10 -0
- package/dist/cli/generate.d.ts +4 -0
- package/dist/cli/generate.js +50 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +57 -0
- package/dist/cli/paths.d.ts +5 -0
- package/dist/cli/paths.js +10 -0
- package/dist/cli/templates.d.ts +6 -0
- package/dist/cli/templates.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +81 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/db/bootstrap-cli.d.ts +1 -0
- package/dist/db/bootstrap-cli.js +17 -0
- package/dist/db/bootstrap.d.ts +3 -0
- package/dist/db/bootstrap.js +16 -0
- package/dist/db/cli.d.ts +1 -0
- package/dist/db/cli.js +20 -0
- package/dist/db/client.d.ts +8 -0
- package/dist/db/client.js +23 -0
- package/dist/db/config.d.ts +1 -0
- package/dist/db/config.js +10 -0
- package/dist/db/db-client-generator.d.ts +13 -0
- package/dist/db/db-client-generator.js +70 -0
- package/dist/db/diff-cli.d.ts +1 -0
- package/dist/db/diff-cli.js +46 -0
- package/dist/db/diff.d.ts +9 -0
- package/dist/db/diff.js +30 -0
- package/dist/db/errors.d.ts +34 -0
- package/dist/db/errors.js +88 -0
- package/dist/db/generate-client-cli.d.ts +1 -0
- package/dist/db/generate-client-cli.js +21 -0
- package/dist/db/index.d.ts +19 -0
- package/dist/db/index.js +17 -0
- package/dist/db/load-env.d.ts +3 -0
- package/dist/db/load-env.js +19 -0
- package/dist/db/migrate-cli.d.ts +1 -0
- package/dist/db/migrate-cli.js +88 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.js +32 -0
- package/dist/db/migrations.d.ts +17 -0
- package/dist/db/migrations.js +81 -0
- package/dist/db/model-client.d.ts +36 -0
- package/dist/db/model-client.js +83 -0
- package/dist/db/model-meta.d.ts +36 -0
- package/dist/db/model-meta.js +57 -0
- package/dist/db/query-builder.d.ts +33 -0
- package/dist/db/query-builder.js +97 -0
- package/dist/db/reset-database.d.ts +4 -0
- package/dist/db/reset-database.js +9 -0
- package/dist/db/row-mapper.d.ts +3 -0
- package/dist/db/row-mapper.js +41 -0
- package/dist/db/schema-state.d.ts +7 -0
- package/dist/db/schema-state.js +32 -0
- package/dist/db/type-generator.d.ts +19 -0
- package/dist/db/type-generator.js +136 -0
- package/dist/db/utils/naming.d.ts +6 -0
- package/dist/db/utils/naming.js +23 -0
- package/dist/db/where-translator.d.ts +20 -0
- package/dist/db/where-translator.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.js +4 -0
- package/dist/schema-dsl/ast.d.ts +108 -0
- package/dist/schema-dsl/ast.js +1 -0
- package/dist/schema-dsl/cli.d.ts +1 -0
- package/dist/schema-dsl/cli.js +25 -0
- package/dist/schema-dsl/index.d.ts +8 -0
- package/dist/schema-dsl/index.js +16 -0
- package/dist/schema-dsl/inspect.d.ts +1 -0
- package/dist/schema-dsl/inspect.js +9 -0
- package/dist/schema-dsl/lexer.d.ts +31 -0
- package/dist/schema-dsl/lexer.js +216 -0
- package/dist/schema-dsl/parser.d.ts +49 -0
- package/dist/schema-dsl/parser.js +372 -0
- package/dist/schema-dsl/tokens.d.ts +30 -0
- package/dist/schema-dsl/tokens.js +35 -0
- package/dist/sql-generator/cli.d.ts +1 -0
- package/dist/sql-generator/cli.js +7 -0
- package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
- package/dist/sql-generator/generators/drop-tables.js +8 -0
- package/dist/sql-generator/generators/enums.d.ts +4 -0
- package/dist/sql-generator/generators/enums.js +16 -0
- package/dist/sql-generator/generators/extensions.d.ts +4 -0
- package/dist/sql-generator/generators/extensions.js +11 -0
- package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
- package/dist/sql-generator/generators/foreign-keys.js +23 -0
- package/dist/sql-generator/generators/indexes.d.ts +13 -0
- package/dist/sql-generator/generators/indexes.js +39 -0
- package/dist/sql-generator/generators/tables.d.ts +4 -0
- package/dist/sql-generator/generators/tables.js +65 -0
- package/dist/sql-generator/generators/triggers.d.ts +6 -0
- package/dist/sql-generator/generators/triggers.js +47 -0
- package/dist/sql-generator/index.d.ts +5 -0
- package/dist/sql-generator/index.js +3 -0
- package/dist/sql-generator/migration-planner.d.ts +15 -0
- package/dist/sql-generator/migration-planner.js +207 -0
- package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
- package/dist/sql-generator/migration-sql-generator.js +181 -0
- package/dist/sql-generator/migration-types.d.ts +86 -0
- package/dist/sql-generator/migration-types.js +1 -0
- package/dist/sql-generator/sql-generator.d.ts +6 -0
- package/dist/sql-generator/sql-generator.js +26 -0
- package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
- package/dist/sql-generator/utils/ast-helpers.js +252 -0
- package/dist/sql-generator/utils/format.d.ts +2 -0
- package/dist/sql-generator/utils/format.js +21 -0
- package/dist/sql-generator/utils/snake-case.d.ts +3 -0
- package/dist/sql-generator/utils/snake-case.js +96 -0
- package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
- package/dist/sql-generator/utils/type-mapper.js +39 -0
- package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
- package/dist/sql-generator/utils/value-formatter.js +41 -0
- package/dist/types/generated-db.stub.d.ts +2 -0
- package/dist/types/generated-db.stub.js +3 -0
- package/dist/types/generated-policies.stub.d.ts +7 -0
- package/dist/types/generated-policies.stub.js +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PACKAGE_NAME } from '../constants.js';
|
|
2
|
+
import { buildModelMetaSnapshot } from './model-meta.js';
|
|
3
|
+
import { getClientExportName, TypeGenerator } from './type-generator.js';
|
|
4
|
+
export class DbClientGenerator {
|
|
5
|
+
schema;
|
|
6
|
+
constructor(schema) {
|
|
7
|
+
this.schema = schema;
|
|
8
|
+
}
|
|
9
|
+
generate() {
|
|
10
|
+
const imports = this.schema.models
|
|
11
|
+
.flatMap((model) => [
|
|
12
|
+
model.name,
|
|
13
|
+
`${model.name}CreateInput`,
|
|
14
|
+
`${model.name}UpdateInput`,
|
|
15
|
+
`${model.name}WhereInput`,
|
|
16
|
+
`${model.name}OrderByInput`,
|
|
17
|
+
])
|
|
18
|
+
.join(',\n ');
|
|
19
|
+
const clientEntries = this.schema.models.map((model) => this.generateClientEntry(model.name));
|
|
20
|
+
return [
|
|
21
|
+
'// Auto-generated by DbClientGenerator. Do not edit manually.',
|
|
22
|
+
"import type { Pool } from 'pg';",
|
|
23
|
+
`import { createModelClient } from '${PACKAGE_NAME}/db/model-client';`,
|
|
24
|
+
`import { hydrateModelMeta } from '${PACKAGE_NAME}/db/model-meta';`,
|
|
25
|
+
`import type {\n ${imports},\n} from './db-types.js';`,
|
|
26
|
+
`import {\n ${this.schema.models.map((model) => `${getClientExportName(model.name)}ModelMeta`).join(',\n ')},\n} from './db-model-meta.js';`,
|
|
27
|
+
'',
|
|
28
|
+
'export function createDbClient(pool: Pool) {',
|
|
29
|
+
...this.schema.models.map((model) => {
|
|
30
|
+
const clientKey = getClientExportName(model.name);
|
|
31
|
+
return ` const ${clientKey}Meta = hydrateModelMeta(${clientKey}ModelMeta);`;
|
|
32
|
+
}),
|
|
33
|
+
'',
|
|
34
|
+
' return {',
|
|
35
|
+
clientEntries.map((entry) => ` ${entry},`).join('\n'),
|
|
36
|
+
' };',
|
|
37
|
+
'}',
|
|
38
|
+
'',
|
|
39
|
+
'export type DbClient = ReturnType<typeof createDbClient>;',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
generateModelMetaModule() {
|
|
44
|
+
const blocks = this.schema.models.map((model) => {
|
|
45
|
+
const meta = buildModelMetaSnapshot(model, this.schema);
|
|
46
|
+
const clientKey = getClientExportName(model.name);
|
|
47
|
+
return `export const ${clientKey}ModelMeta = ${JSON.stringify(meta, null, 2)} as const;`;
|
|
48
|
+
});
|
|
49
|
+
return [
|
|
50
|
+
'// Auto-generated model metadata. Do not edit manually.',
|
|
51
|
+
`import type { ModelMetaSnapshot } from '${PACKAGE_NAME}/db/model-meta';`,
|
|
52
|
+
'',
|
|
53
|
+
...blocks,
|
|
54
|
+
'',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
generateClientEntry(modelName) {
|
|
58
|
+
const clientKey = getClientExportName(modelName);
|
|
59
|
+
return `${clientKey}: createModelClient<${modelName}, ${modelName}CreateInput, ${modelName}UpdateInput, ${modelName}WhereInput, ${modelName}OrderByInput>(${clientKey}Meta, pool)`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function generateDbClientFiles(schema) {
|
|
63
|
+
const typeGenerator = new TypeGenerator(schema);
|
|
64
|
+
const dbClientGenerator = new DbClientGenerator(schema);
|
|
65
|
+
return {
|
|
66
|
+
dbTypes: typeGenerator.generate(),
|
|
67
|
+
dbClient: dbClientGenerator.generate(),
|
|
68
|
+
modelMeta: dbClientGenerator.generateModelMetaModule(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createMigration } from './migrations.js';
|
|
2
|
+
import { defaultSchemaPath, generateSchemaDiff } from './diff.js';
|
|
3
|
+
function parseArgs(argv) {
|
|
4
|
+
const args = argv.slice(2);
|
|
5
|
+
let schemaPath = defaultSchemaPath();
|
|
6
|
+
let name;
|
|
7
|
+
let print = false;
|
|
8
|
+
for (let index = 0; index < args.length; index++) {
|
|
9
|
+
const arg = args[index];
|
|
10
|
+
if (arg === '--print') {
|
|
11
|
+
print = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === '--name') {
|
|
15
|
+
name = args[index + 1];
|
|
16
|
+
index++;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!arg.startsWith('--')) {
|
|
20
|
+
schemaPath = arg;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { schemaPath, name, print };
|
|
24
|
+
}
|
|
25
|
+
async function main() {
|
|
26
|
+
const { schemaPath, name, print } = parseArgs(process.argv);
|
|
27
|
+
const diff = generateSchemaDiff(schemaPath);
|
|
28
|
+
if (diff.migrations.length === 0) {
|
|
29
|
+
process.stdout.write('No schema changes detected.\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (diff.hasDestructiveChanges) {
|
|
33
|
+
process.stderr.write('Warning: migration includes destructive changes (drops).\n');
|
|
34
|
+
}
|
|
35
|
+
if (print || !name) {
|
|
36
|
+
process.stdout.write(diff.sql);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const migration = createMigration(name, diff.sql);
|
|
40
|
+
process.stdout.write(`Migration written to ${migration.path}\n`);
|
|
41
|
+
}
|
|
42
|
+
main().catch((error) => {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
process.stderr.write(`Schema diff failed: ${message}\n`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Migration } from '../sql-generator/migration-types.js';
|
|
2
|
+
export interface DiffResult {
|
|
3
|
+
migrations: Migration[];
|
|
4
|
+
sql: string;
|
|
5
|
+
hasDestructiveChanges: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateSchemaDiff(schemaPath: string, cwd?: string): DiffResult;
|
|
8
|
+
export declare function summarizeMigrations(migrations: Migration[]): Map<string, number>;
|
|
9
|
+
export declare function defaultSchemaPath(cwd?: string): string;
|
package/dist/db/diff.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from '../schema-dsl/index.js';
|
|
4
|
+
import { MigrationPlanner, MigrationSqlGenerator } from '../sql-generator/index.js';
|
|
5
|
+
import { DESTRUCTIVE_MIGRATION_KINDS } from './migrations.js';
|
|
6
|
+
import { readSnapshotSource } from './schema-state.js';
|
|
7
|
+
export function generateSchemaDiff(schemaPath, cwd = process.cwd()) {
|
|
8
|
+
const snapshotSource = readSnapshotSource(cwd);
|
|
9
|
+
if (!snapshotSource) {
|
|
10
|
+
throw new Error(`No schema snapshot found at .schema-state/app.schema. Run db:bootstrap or copy app.schema to initialize the snapshot.`);
|
|
11
|
+
}
|
|
12
|
+
const oldSchema = parse(snapshotSource);
|
|
13
|
+
const newSource = readFileSync(schemaPath, 'utf8');
|
|
14
|
+
const newSchema = parse(newSource);
|
|
15
|
+
const planner = new MigrationPlanner();
|
|
16
|
+
const migrations = planner.generateMigration(oldSchema, newSchema);
|
|
17
|
+
const sql = new MigrationSqlGenerator().generate(migrations, newSchema);
|
|
18
|
+
const hasDestructiveChanges = migrations.some((migration) => DESTRUCTIVE_MIGRATION_KINDS.has(migration.kind));
|
|
19
|
+
return { migrations, sql, hasDestructiveChanges };
|
|
20
|
+
}
|
|
21
|
+
export function summarizeMigrations(migrations) {
|
|
22
|
+
const counts = new Map();
|
|
23
|
+
for (const migration of migrations) {
|
|
24
|
+
counts.set(migration.kind, (counts.get(migration.kind) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
return counts;
|
|
27
|
+
}
|
|
28
|
+
export function defaultSchemaPath(cwd = process.cwd()) {
|
|
29
|
+
return join(cwd, 'app.schema');
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare class DatabaseError extends Error {
|
|
2
|
+
readonly code?: string;
|
|
3
|
+
readonly detail?: string;
|
|
4
|
+
readonly constraint?: string;
|
|
5
|
+
constructor(message: string, options?: {
|
|
6
|
+
code?: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
constraint?: string;
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export declare class UniqueConstraintError extends DatabaseError {
|
|
13
|
+
readonly fields: string[];
|
|
14
|
+
constructor(fields: string[], options?: {
|
|
15
|
+
constraint?: string;
|
|
16
|
+
detail?: string;
|
|
17
|
+
cause?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export declare class ForeignKeyConstraintError extends DatabaseError {
|
|
21
|
+
readonly field?: string;
|
|
22
|
+
constructor(message: string, options?: {
|
|
23
|
+
field?: string;
|
|
24
|
+
constraint?: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
cause?: unknown;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export declare class NotFoundError extends DatabaseError {
|
|
30
|
+
readonly model: string;
|
|
31
|
+
readonly where: Record<string, unknown>;
|
|
32
|
+
constructor(model: string, where: Record<string, unknown>);
|
|
33
|
+
}
|
|
34
|
+
export declare function mapPgError(error: unknown, modelName: string, columnToField: Map<string, string>): DatabaseError;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export class DatabaseError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
detail;
|
|
4
|
+
constraint;
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, { cause: options?.cause });
|
|
7
|
+
this.name = 'DatabaseError';
|
|
8
|
+
this.code = options?.code;
|
|
9
|
+
this.detail = options?.detail;
|
|
10
|
+
this.constraint = options?.constraint;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UniqueConstraintError extends DatabaseError {
|
|
14
|
+
fields;
|
|
15
|
+
constructor(fields, options) {
|
|
16
|
+
super(`Unique constraint violation on ${fields.join(', ')}`, {
|
|
17
|
+
code: '23505',
|
|
18
|
+
constraint: options?.constraint,
|
|
19
|
+
detail: options?.detail,
|
|
20
|
+
cause: options?.cause,
|
|
21
|
+
});
|
|
22
|
+
this.name = 'UniqueConstraintError';
|
|
23
|
+
this.fields = fields;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class ForeignKeyConstraintError extends DatabaseError {
|
|
27
|
+
field;
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super(message, {
|
|
30
|
+
code: '23503',
|
|
31
|
+
constraint: options?.constraint,
|
|
32
|
+
detail: options?.detail,
|
|
33
|
+
cause: options?.cause,
|
|
34
|
+
});
|
|
35
|
+
this.name = 'ForeignKeyConstraintError';
|
|
36
|
+
this.field = options?.field;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class NotFoundError extends DatabaseError {
|
|
40
|
+
model;
|
|
41
|
+
where;
|
|
42
|
+
constructor(model, where) {
|
|
43
|
+
super(`Record not found in ${model}`);
|
|
44
|
+
this.name = 'NotFoundError';
|
|
45
|
+
this.model = model;
|
|
46
|
+
this.where = where;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function mapPgError(error, modelName, columnToField) {
|
|
50
|
+
if (!(error && typeof error === 'object' && 'code' in error)) {
|
|
51
|
+
return new DatabaseError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
52
|
+
}
|
|
53
|
+
const pgError = error;
|
|
54
|
+
if (pgError.code === '23505') {
|
|
55
|
+
const fields = extractConstraintFields(pgError.detail, columnToField);
|
|
56
|
+
return new UniqueConstraintError(fields, {
|
|
57
|
+
constraint: pgError.constraint,
|
|
58
|
+
detail: pgError.detail,
|
|
59
|
+
cause: error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (pgError.code === '23503') {
|
|
63
|
+
return new ForeignKeyConstraintError(pgError.message ?? 'Foreign key constraint violation', {
|
|
64
|
+
constraint: pgError.constraint,
|
|
65
|
+
detail: pgError.detail,
|
|
66
|
+
cause: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return new DatabaseError(pgError.message ?? 'Database error', {
|
|
70
|
+
code: pgError.code,
|
|
71
|
+
detail: pgError.detail,
|
|
72
|
+
constraint: pgError.constraint,
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function extractConstraintFields(detail, columnToField) {
|
|
77
|
+
if (!detail) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const match = detail.match(/\(([^)]+)\)=\(/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return match[1]
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((column) => column.trim())
|
|
87
|
+
.map((column) => columnToField.get(column) ?? column);
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '../schema-dsl/index.js';
|
|
4
|
+
import { generateDbClientFiles } from './db-client-generator.js';
|
|
5
|
+
const DEFAULT_SCHEMA_PATH = path.resolve('app.schema');
|
|
6
|
+
const OUTPUT_DIR = path.resolve('generated');
|
|
7
|
+
async function main() {
|
|
8
|
+
const schemaPath = process.argv[2] ?? DEFAULT_SCHEMA_PATH;
|
|
9
|
+
const source = await readFile(schemaPath, 'utf8');
|
|
10
|
+
const schema = parse(source);
|
|
11
|
+
const files = generateDbClientFiles(schema);
|
|
12
|
+
await mkdir(OUTPUT_DIR, { recursive: true });
|
|
13
|
+
await writeFile(path.join(OUTPUT_DIR, 'db-types.ts'), files.dbTypes, 'utf8');
|
|
14
|
+
await writeFile(path.join(OUTPUT_DIR, 'db-model-meta.ts'), files.modelMeta, 'utf8');
|
|
15
|
+
await writeFile(path.join(OUTPUT_DIR, 'db.ts'), files.dbClient, 'utf8');
|
|
16
|
+
console.log(`Generated db client files in ${OUTPUT_DIR}`);
|
|
17
|
+
}
|
|
18
|
+
main().catch((error) => {
|
|
19
|
+
console.error(error instanceof Error ? error.message : error);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { bootstrapDatabase, generateBootstrapSql } from './bootstrap.js';
|
|
2
|
+
export { DatabaseClient } from './client.js';
|
|
3
|
+
export { getDatabaseUrl } from './config.js';
|
|
4
|
+
export { QueryBuilder } from './query-builder.js';
|
|
5
|
+
export { WhereTranslator } from './where-translator.js';
|
|
6
|
+
export { createModelClient } from './model-client.js';
|
|
7
|
+
export { TypeGenerator, generateDbTypes } from './type-generator.js';
|
|
8
|
+
export { DbClientGenerator, generateDbClientFiles } from './db-client-generator.js';
|
|
9
|
+
export { buildModelMeta, buildModelMetaSnapshot, hydrateModelMeta } from './model-meta.js';
|
|
10
|
+
export { DatabaseError, UniqueConstraintError, ForeignKeyConstraintError, NotFoundError, mapPgError, } from './errors.js';
|
|
11
|
+
export { mapRow, mapRows } from './row-mapper.js';
|
|
12
|
+
export { toCamelCase, pluralize, toTableName, toColumnName, toClientKey } from './utils/naming.js';
|
|
13
|
+
export { loadEnv, resetLoadEnvForTests } from './load-env.js';
|
|
14
|
+
export { defaultSchemaPath, generateSchemaDiff, summarizeMigrations } from './diff.js';
|
|
15
|
+
export type { DiffResult } from './diff.js';
|
|
16
|
+
export { applyPendingMigrations } from './migrate.js';
|
|
17
|
+
export { createMigration, ensureMigrationsDir, getMigrationsDir, listMigrationFiles, listPendingMigrations, readMigrationSql, DESTRUCTIVE_MIGRATION_KINDS, } from './migrations.js';
|
|
18
|
+
export type { MigrationFile } from './migrations.js';
|
|
19
|
+
export { ensureSnapshot, getSnapshotPath, readSnapshotSchema, readSnapshotSource, snapshotExists, writeSnapshot, } from './schema-state.js';
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { bootstrapDatabase, generateBootstrapSql } from './bootstrap.js';
|
|
2
|
+
export { DatabaseClient } from './client.js';
|
|
3
|
+
export { getDatabaseUrl } from './config.js';
|
|
4
|
+
export { QueryBuilder } from './query-builder.js';
|
|
5
|
+
export { WhereTranslator } from './where-translator.js';
|
|
6
|
+
export { createModelClient } from './model-client.js';
|
|
7
|
+
export { TypeGenerator, generateDbTypes } from './type-generator.js';
|
|
8
|
+
export { DbClientGenerator, generateDbClientFiles } from './db-client-generator.js';
|
|
9
|
+
export { buildModelMeta, buildModelMetaSnapshot, hydrateModelMeta } from './model-meta.js';
|
|
10
|
+
export { DatabaseError, UniqueConstraintError, ForeignKeyConstraintError, NotFoundError, mapPgError, } from './errors.js';
|
|
11
|
+
export { mapRow, mapRows } from './row-mapper.js';
|
|
12
|
+
export { toCamelCase, pluralize, toTableName, toColumnName, toClientKey } from './utils/naming.js';
|
|
13
|
+
export { loadEnv, resetLoadEnvForTests } from './load-env.js';
|
|
14
|
+
export { defaultSchemaPath, generateSchemaDiff, summarizeMigrations } from './diff.js';
|
|
15
|
+
export { applyPendingMigrations } from './migrate.js';
|
|
16
|
+
export { createMigration, ensureMigrationsDir, getMigrationsDir, listMigrationFiles, listPendingMigrations, readMigrationSql, DESTRUCTIVE_MIGRATION_KINDS, } from './migrations.js';
|
|
17
|
+
export { ensureSnapshot, getSnapshotPath, readSnapshotSchema, readSnapshotSource, snapshotExists, writeSnapshot, } from './schema-state.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
let loaded = false;
|
|
4
|
+
export function loadEnv() {
|
|
5
|
+
if (loaded) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const envPath = resolve(process.cwd(), '.env');
|
|
9
|
+
if (existsSync(envPath)) {
|
|
10
|
+
process.loadEnvFile(envPath);
|
|
11
|
+
}
|
|
12
|
+
loaded = true;
|
|
13
|
+
}
|
|
14
|
+
export function resetLoadEnvForTests() {
|
|
15
|
+
loaded = false;
|
|
16
|
+
}
|
|
17
|
+
export function markEnvLoadedForTests() {
|
|
18
|
+
loaded = true;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DatabaseClient } from './client.js';
|
|
2
|
+
import { applyPendingMigrations } from './migrate.js';
|
|
3
|
+
import { getAppliedMigrationFilenames, listMigrationFiles } from './migrations.js';
|
|
4
|
+
import { defaultSchemaPath, generateSchemaDiff, summarizeMigrations } from './diff.js';
|
|
5
|
+
import { snapshotExists } from './schema-state.js';
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = argv.slice(2);
|
|
8
|
+
const command = args[0] === 'status' ? 'status' : 'migrate';
|
|
9
|
+
const schemaArg = args.find((arg) => !arg.startsWith('--') && arg !== 'status');
|
|
10
|
+
const schemaPath = schemaArg ?? defaultSchemaPath();
|
|
11
|
+
return { command, schemaPath };
|
|
12
|
+
}
|
|
13
|
+
async function showStatus(schemaPath, client) {
|
|
14
|
+
if (!snapshotExists()) {
|
|
15
|
+
process.stdout.write('Snapshot: missing (.schema-state/app.schema)\n');
|
|
16
|
+
process.stdout.write('\nPending schema changes (snapshot vs app.schema):\n');
|
|
17
|
+
process.stdout.write(' (snapshot not initialized — run db:bootstrap first)\n');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
process.stdout.write('Snapshot: present\n');
|
|
21
|
+
try {
|
|
22
|
+
const diff = generateSchemaDiff(schemaPath);
|
|
23
|
+
const counts = summarizeMigrations(diff.migrations);
|
|
24
|
+
process.stdout.write('\nPending schema changes (snapshot vs app.schema):\n');
|
|
25
|
+
if (counts.size === 0) {
|
|
26
|
+
process.stdout.write(' (none)\n');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
for (const [kind, count] of [...counts.entries()].sort()) {
|
|
30
|
+
process.stdout.write(` ${kind}: ${count}\n`);
|
|
31
|
+
}
|
|
32
|
+
if (diff.hasDestructiveChanges) {
|
|
33
|
+
process.stdout.write(' Warning: includes destructive changes\n');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
process.stdout.write(`\nPending schema changes: error — ${message}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const applied = await client.withClient(async (pgClient) => getAppliedMigrationFilenames(pgClient));
|
|
43
|
+
const files = listMigrationFiles();
|
|
44
|
+
const pendingFiles = files.filter((migration) => !applied.has(migration.filename));
|
|
45
|
+
process.stdout.write('\nMigration files:\n');
|
|
46
|
+
if (files.length === 0) {
|
|
47
|
+
process.stdout.write(' (none)\n');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const migration of files) {
|
|
51
|
+
const state = applied.has(migration.filename) ? 'applied' : 'pending';
|
|
52
|
+
process.stdout.write(` [${state}] ${migration.filename}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (pendingFiles.length > 0) {
|
|
56
|
+
process.stdout.write(`\n${pendingFiles.length} migration file(s) pending apply.\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function runMigrate(schemaPath, client) {
|
|
60
|
+
const applied = await applyPendingMigrations(schemaPath, client);
|
|
61
|
+
if (applied.length === 0) {
|
|
62
|
+
process.stdout.write('No pending migrations to apply.\n');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const migration of applied) {
|
|
66
|
+
process.stdout.write(`Applied ${migration.filename}\n`);
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(`Snapshot updated from ${schemaPath}\n`);
|
|
69
|
+
}
|
|
70
|
+
async function main() {
|
|
71
|
+
const { command, schemaPath } = parseArgs(process.argv);
|
|
72
|
+
const client = new DatabaseClient();
|
|
73
|
+
try {
|
|
74
|
+
if (command === 'status') {
|
|
75
|
+
await showStatus(schemaPath, client);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await runMigrate(schemaPath, client);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await client.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main().catch((error) => {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
process.stderr.write(`Migration failed: ${message}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DatabaseClient } from './client.js';
|
|
3
|
+
import { listPendingMigrations, readMigrationSql, recordAppliedMigration, } from './migrations.js';
|
|
4
|
+
import { writeSnapshot } from './schema-state.js';
|
|
5
|
+
export async function applyPendingMigrations(schemaPath = join(process.cwd(), 'app.schema'), client = new DatabaseClient(), cwd = process.cwd()) {
|
|
6
|
+
const applied = [];
|
|
7
|
+
await client.withClient(async (pgClient) => {
|
|
8
|
+
const pending = await listPendingMigrations(pgClient, cwd);
|
|
9
|
+
if (pending.length === 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
for (const migration of pending) {
|
|
13
|
+
await applyMigration(pgClient, migration);
|
|
14
|
+
applied.push(migration);
|
|
15
|
+
}
|
|
16
|
+
writeSnapshot(schemaPath, cwd);
|
|
17
|
+
});
|
|
18
|
+
return applied;
|
|
19
|
+
}
|
|
20
|
+
async function applyMigration(client, migration) {
|
|
21
|
+
const sql = readMigrationSql(migration);
|
|
22
|
+
await client.query('BEGIN');
|
|
23
|
+
try {
|
|
24
|
+
await client.query(sql);
|
|
25
|
+
await recordAppliedMigration(client, migration);
|
|
26
|
+
await client.query('COMMIT');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
await client.query('ROLLBACK');
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PoolClient } from 'pg';
|
|
2
|
+
export interface MigrationFile {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getMigrationsDir(cwd?: string): string;
|
|
9
|
+
export declare function ensureMigrationsDir(cwd?: string): string;
|
|
10
|
+
export declare function listMigrationFiles(cwd?: string): MigrationFile[];
|
|
11
|
+
export declare function createMigration(name: string, sql: string, cwd?: string): MigrationFile;
|
|
12
|
+
export declare function ensureMigrationsTable(client: PoolClient): Promise<void>;
|
|
13
|
+
export declare function getAppliedMigrationFilenames(client: PoolClient): Promise<Set<string>>;
|
|
14
|
+
export declare function listPendingMigrations(client: PoolClient, cwd?: string): Promise<MigrationFile[]>;
|
|
15
|
+
export declare function recordAppliedMigration(client: PoolClient, migration: MigrationFile): Promise<void>;
|
|
16
|
+
export declare function readMigrationSql(migration: MigrationFile): string;
|
|
17
|
+
export declare const DESTRUCTIVE_MIGRATION_KINDS: Set<string>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const MIGRATIONS_DIR = 'migrations';
|
|
4
|
+
const MIGRATIONS_TABLE = '_schema_migrations';
|
|
5
|
+
export function getMigrationsDir(cwd = process.cwd()) {
|
|
6
|
+
return join(cwd, MIGRATIONS_DIR);
|
|
7
|
+
}
|
|
8
|
+
export function ensureMigrationsDir(cwd = process.cwd()) {
|
|
9
|
+
const dir = getMigrationsDir(cwd);
|
|
10
|
+
mkdirSync(dir, { recursive: true });
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
export function listMigrationFiles(cwd = process.cwd()) {
|
|
14
|
+
const dir = getMigrationsDir(cwd);
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return readdirSync(dir)
|
|
19
|
+
.filter((filename) => filename.endsWith('.sql'))
|
|
20
|
+
.map((filename) => {
|
|
21
|
+
const match = /^(\d+)_(.+)\.sql$/.exec(filename);
|
|
22
|
+
if (!match) {
|
|
23
|
+
throw new Error(`Invalid migration filename: ${filename}`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
id: Number(match[1]),
|
|
27
|
+
name: match[2],
|
|
28
|
+
filename,
|
|
29
|
+
path: join(dir, filename),
|
|
30
|
+
};
|
|
31
|
+
})
|
|
32
|
+
.sort((left, right) => left.id - right.id);
|
|
33
|
+
}
|
|
34
|
+
export function createMigration(name, sql, cwd = process.cwd()) {
|
|
35
|
+
const dir = ensureMigrationsDir(cwd);
|
|
36
|
+
const existing = listMigrationFiles(cwd);
|
|
37
|
+
const nextId = existing.length > 0 ? existing[existing.length - 1].id + 1 : 1;
|
|
38
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
39
|
+
const filename = `${String(nextId).padStart(4, '0')}_${sanitizedName}.sql`;
|
|
40
|
+
const path = join(dir, filename);
|
|
41
|
+
writeFileSync(path, sql, 'utf8');
|
|
42
|
+
return {
|
|
43
|
+
id: nextId,
|
|
44
|
+
name: sanitizedName,
|
|
45
|
+
filename,
|
|
46
|
+
path,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function ensureMigrationsTable(client) {
|
|
50
|
+
await client.query(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
52
|
+
id INTEGER PRIMARY KEY,
|
|
53
|
+
name TEXT NOT NULL,
|
|
54
|
+
filename TEXT NOT NULL UNIQUE,
|
|
55
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
export async function getAppliedMigrationFilenames(client) {
|
|
60
|
+
await ensureMigrationsTable(client);
|
|
61
|
+
const result = await client.query(`SELECT filename FROM ${MIGRATIONS_TABLE} ORDER BY id`);
|
|
62
|
+
return new Set(result.rows.map((row) => row.filename));
|
|
63
|
+
}
|
|
64
|
+
export async function listPendingMigrations(client, cwd = process.cwd()) {
|
|
65
|
+
const applied = await getAppliedMigrationFilenames(client);
|
|
66
|
+
return listMigrationFiles(cwd).filter((migration) => !applied.has(migration.filename));
|
|
67
|
+
}
|
|
68
|
+
export async function recordAppliedMigration(client, migration) {
|
|
69
|
+
await client.query(`INSERT INTO ${MIGRATIONS_TABLE} (id, name, filename) VALUES ($1, $2, $3)`, [migration.id, migration.name, migration.filename]);
|
|
70
|
+
}
|
|
71
|
+
export function readMigrationSql(migration) {
|
|
72
|
+
return readFileSync(migration.path, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
export const DESTRUCTIVE_MIGRATION_KINDS = new Set([
|
|
75
|
+
'DropTable',
|
|
76
|
+
'DropColumn',
|
|
77
|
+
'DropConstraint',
|
|
78
|
+
'DropIndex',
|
|
79
|
+
'DropExtension',
|
|
80
|
+
'DropTrigger',
|
|
81
|
+
]);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { ModelMeta } from './model-meta.js';
|
|
3
|
+
export interface ModelClient<T, TCreate, TUpdate, TWhere, TOrderBy> {
|
|
4
|
+
create(data: TCreate): Promise<T>;
|
|
5
|
+
findUnique(where: Record<string, unknown>): Promise<T | null>;
|
|
6
|
+
findFirst(args?: {
|
|
7
|
+
where?: TWhere;
|
|
8
|
+
orderBy?: TOrderBy;
|
|
9
|
+
}): Promise<T | null>;
|
|
10
|
+
findMany(args?: {
|
|
11
|
+
where?: TWhere;
|
|
12
|
+
orderBy?: TOrderBy | TOrderBy[];
|
|
13
|
+
take?: number;
|
|
14
|
+
skip?: number;
|
|
15
|
+
}): Promise<T[]>;
|
|
16
|
+
count(args?: {
|
|
17
|
+
where?: TWhere;
|
|
18
|
+
}): Promise<number>;
|
|
19
|
+
update(args: {
|
|
20
|
+
where: Record<string, unknown>;
|
|
21
|
+
data: TUpdate;
|
|
22
|
+
}): Promise<T>;
|
|
23
|
+
updateMany(args: {
|
|
24
|
+
where?: TWhere;
|
|
25
|
+
data: TUpdate;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
count: number;
|
|
28
|
+
}>;
|
|
29
|
+
delete(where: Record<string, unknown>): Promise<T>;
|
|
30
|
+
deleteMany(args?: {
|
|
31
|
+
where?: TWhere;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
count: number;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function createModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>(model: ModelMeta, pool: Pool): ModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>;
|