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,47 @@
|
|
|
1
|
+
import { getDirectives, normalizeTriggerDirective, resolveTriggerNames, } from '../utils/ast-helpers.js';
|
|
2
|
+
import { joinSection } from '../utils/format.js';
|
|
3
|
+
import { quoteIdentifier, toTableName } from '../utils/snake-case.js';
|
|
4
|
+
export function generateCreateTrigger(model, normalized) {
|
|
5
|
+
const tableName = quoteIdentifier(toTableName(model.name));
|
|
6
|
+
const { functionName, triggerName } = resolveTriggerNames(model, normalized.timing, normalized.event);
|
|
7
|
+
const forEachClause = normalized.level === 'STATEMENT' ? 'FOR EACH STATEMENT' : 'FOR EACH ROW';
|
|
8
|
+
const functionStatement = [
|
|
9
|
+
`CREATE OR REPLACE FUNCTION ${functionName}()`,
|
|
10
|
+
'RETURNS TRIGGER AS $$',
|
|
11
|
+
'BEGIN',
|
|
12
|
+
indentTriggerBody(normalized.execute),
|
|
13
|
+
'END;',
|
|
14
|
+
'$$ LANGUAGE plpgsql;',
|
|
15
|
+
].join('\n');
|
|
16
|
+
const triggerStatement = [
|
|
17
|
+
`CREATE TRIGGER ${triggerName}`,
|
|
18
|
+
` ${normalized.timing} ${normalized.event} ON ${tableName}`,
|
|
19
|
+
` ${forEachClause}`,
|
|
20
|
+
` EXECUTE FUNCTION ${functionName}();`,
|
|
21
|
+
].join('\n');
|
|
22
|
+
return `${functionStatement}\n\n${triggerStatement}`;
|
|
23
|
+
}
|
|
24
|
+
export function generateDropTrigger(model, normalized) {
|
|
25
|
+
const tableName = quoteIdentifier(toTableName(model.name));
|
|
26
|
+
const { functionName, triggerName } = resolveTriggerNames(model, normalized.timing, normalized.event);
|
|
27
|
+
return [
|
|
28
|
+
`DROP TRIGGER IF EXISTS ${triggerName} ON ${tableName};`,
|
|
29
|
+
`DROP FUNCTION IF EXISTS ${functionName}();`,
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
export function generateTriggers(schema) {
|
|
33
|
+
const statements = [];
|
|
34
|
+
for (const model of schema.models) {
|
|
35
|
+
for (const directive of getDirectives(model, 'trigger')) {
|
|
36
|
+
const normalized = normalizeTriggerDirective(directive);
|
|
37
|
+
statements.push(generateCreateTrigger(model, normalized));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return joinSection('Create triggers', statements);
|
|
41
|
+
}
|
|
42
|
+
function indentTriggerBody(body) {
|
|
43
|
+
return body
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((line) => (line.length > 0 ? ` ${line}` : line))
|
|
46
|
+
.join('\n');
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SqlGenerator, toSnakeCase, toTableName } from './sql-generator.js';
|
|
2
|
+
export { MigrationPlanner, getStoredFieldNames } from './migration-planner.js';
|
|
3
|
+
export { MigrationSqlGenerator } from './migration-sql-generator.js';
|
|
4
|
+
export type { Migration } from './migration-types.js';
|
|
5
|
+
export type { Schema } from '../schema-dsl/ast.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Model, Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
import type { Migration } from './migration-types.js';
|
|
3
|
+
export declare class MigrationPlanner {
|
|
4
|
+
generateMigration(oldSchema: Schema, newSchema: Schema): Migration[];
|
|
5
|
+
private diffExtensions;
|
|
6
|
+
private diffEnums;
|
|
7
|
+
private diffModels;
|
|
8
|
+
private diffField;
|
|
9
|
+
private diffConstraints;
|
|
10
|
+
private diffIndexes;
|
|
11
|
+
private diffTriggers;
|
|
12
|
+
private triggerSignatures;
|
|
13
|
+
private indexSignatures;
|
|
14
|
+
}
|
|
15
|
+
export declare function getStoredFieldNames(model: Model, modelNames: Set<string>): string[];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { collectForeignKeys, getDirectives, getEnumNames, getModelNames, getStoredFields, isStoredField, normalizeIndexDirective, normalizeTriggerDirective, serializeColumnType, serializeDefault, serializeForeignKey, } from './utils/ast-helpers.js';
|
|
2
|
+
export class MigrationPlanner {
|
|
3
|
+
generateMigration(oldSchema, newSchema) {
|
|
4
|
+
const migrations = [];
|
|
5
|
+
migrations.push(...this.diffExtensions(oldSchema, newSchema));
|
|
6
|
+
migrations.push(...this.diffEnums(oldSchema, newSchema));
|
|
7
|
+
migrations.push(...this.diffModels(oldSchema, newSchema));
|
|
8
|
+
migrations.push(...this.diffConstraints(oldSchema, newSchema));
|
|
9
|
+
migrations.push(...this.diffIndexes(oldSchema, newSchema));
|
|
10
|
+
migrations.push(...this.diffTriggers(oldSchema, newSchema));
|
|
11
|
+
return migrations;
|
|
12
|
+
}
|
|
13
|
+
diffExtensions(oldSchema, newSchema) {
|
|
14
|
+
const migrations = [];
|
|
15
|
+
const oldNames = new Set(oldSchema.extensions.map((extension) => extension.name));
|
|
16
|
+
const newNames = new Set(newSchema.extensions.map((extension) => extension.name));
|
|
17
|
+
for (const name of newNames) {
|
|
18
|
+
if (!oldNames.has(name)) {
|
|
19
|
+
migrations.push({ kind: 'CreateExtension', extensionName: name });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const name of oldNames) {
|
|
23
|
+
if (!newNames.has(name)) {
|
|
24
|
+
migrations.push({ kind: 'DropExtension', extensionName: name });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return migrations;
|
|
28
|
+
}
|
|
29
|
+
diffEnums(oldSchema, newSchema) {
|
|
30
|
+
const migrations = [];
|
|
31
|
+
const oldEnums = new Map(oldSchema.enums.map((enumDef) => [enumDef.name, enumDef]));
|
|
32
|
+
const newEnums = new Map(newSchema.enums.map((enumDef) => [enumDef.name, enumDef]));
|
|
33
|
+
for (const [enumName, enumDef] of newEnums) {
|
|
34
|
+
if (!oldEnums.has(enumName)) {
|
|
35
|
+
migrations.push({ kind: 'CreateEnum', enumName });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const oldValues = oldEnums.get(enumName).values;
|
|
39
|
+
for (const value of enumDef.values) {
|
|
40
|
+
if (!oldValues.includes(value)) {
|
|
41
|
+
migrations.push({ kind: 'AddEnumValue', enumName, value });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return migrations;
|
|
46
|
+
}
|
|
47
|
+
diffModels(oldSchema, newSchema) {
|
|
48
|
+
const migrations = [];
|
|
49
|
+
const oldModels = new Map(oldSchema.models.map((model) => [model.name, model]));
|
|
50
|
+
const newModels = new Map(newSchema.models.map((model) => [model.name, model]));
|
|
51
|
+
const oldModelNames = getModelNames(oldSchema);
|
|
52
|
+
const newModelNames = getModelNames(newSchema);
|
|
53
|
+
const oldEnumNames = getEnumNames(oldSchema);
|
|
54
|
+
const newEnumNames = getEnumNames(newSchema);
|
|
55
|
+
for (const [modelName] of newModels) {
|
|
56
|
+
if (!oldModels.has(modelName)) {
|
|
57
|
+
migrations.push({ kind: 'CreateTable', modelName });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const [modelName] of oldModels) {
|
|
61
|
+
if (!newModels.has(modelName)) {
|
|
62
|
+
migrations.push({ kind: 'DropTable', modelName });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const [modelName, newModel] of newModels) {
|
|
66
|
+
const oldModel = oldModels.get(modelName);
|
|
67
|
+
if (!oldModel) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const oldFields = new Map(getStoredFields(oldModel, oldModelNames).map((field) => [field.name, field]));
|
|
71
|
+
const newFields = new Map(getStoredFields(newModel, newModelNames).map((field) => [field.name, field]));
|
|
72
|
+
for (const [fieldName] of newFields) {
|
|
73
|
+
if (!oldFields.has(fieldName)) {
|
|
74
|
+
migrations.push({ kind: 'AddColumn', modelName, fieldName });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [fieldName] of oldFields) {
|
|
78
|
+
if (!newFields.has(fieldName)) {
|
|
79
|
+
migrations.push({ kind: 'DropColumn', modelName, fieldName });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [fieldName, newField] of newFields) {
|
|
83
|
+
const oldField = oldFields.get(fieldName);
|
|
84
|
+
if (!oldField) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
migrations.push(...this.diffField(modelName, oldField, newField, oldEnumNames, newEnumNames));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return migrations;
|
|
91
|
+
}
|
|
92
|
+
diffField(modelName, oldField, newField, oldEnumNames, newEnumNames) {
|
|
93
|
+
const migrations = [];
|
|
94
|
+
const oldType = serializeColumnType(oldField.type, oldEnumNames);
|
|
95
|
+
const newType = serializeColumnType(newField.type, newEnumNames);
|
|
96
|
+
if (oldType !== newType) {
|
|
97
|
+
migrations.push({
|
|
98
|
+
kind: 'AlterColumn',
|
|
99
|
+
modelName,
|
|
100
|
+
fieldName: oldField.name,
|
|
101
|
+
change: { type: 'type', from: oldType, to: newType },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const oldOptional = Boolean(oldField.type.optional);
|
|
105
|
+
const newOptional = Boolean(newField.type.optional);
|
|
106
|
+
if (oldOptional !== newOptional) {
|
|
107
|
+
migrations.push({
|
|
108
|
+
kind: 'AlterColumn',
|
|
109
|
+
modelName,
|
|
110
|
+
fieldName: oldField.name,
|
|
111
|
+
change: { type: 'nullability', from: oldOptional, to: newOptional },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const oldDefault = serializeDefault(oldField, oldEnumNames);
|
|
115
|
+
const newDefault = serializeDefault(newField, newEnumNames);
|
|
116
|
+
if (oldDefault !== newDefault) {
|
|
117
|
+
migrations.push({
|
|
118
|
+
kind: 'AlterColumn',
|
|
119
|
+
modelName,
|
|
120
|
+
fieldName: oldField.name,
|
|
121
|
+
change: { type: 'default', from: oldDefault, to: newDefault },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return migrations;
|
|
125
|
+
}
|
|
126
|
+
diffConstraints(oldSchema, newSchema) {
|
|
127
|
+
const migrations = [];
|
|
128
|
+
const oldKeys = new Map(collectForeignKeys(oldSchema).map((foreignKey) => [serializeForeignKey(foreignKey), foreignKey]));
|
|
129
|
+
const newKeys = new Map(collectForeignKeys(newSchema).map((foreignKey) => [serializeForeignKey(foreignKey), foreignKey]));
|
|
130
|
+
for (const [signature, foreignKey] of newKeys) {
|
|
131
|
+
if (!oldKeys.has(signature)) {
|
|
132
|
+
migrations.push({
|
|
133
|
+
kind: 'AddConstraint',
|
|
134
|
+
modelName: foreignKey.sourceModel,
|
|
135
|
+
constraintType: 'foreignKey',
|
|
136
|
+
details: signature,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const [signature, foreignKey] of oldKeys) {
|
|
141
|
+
if (!newKeys.has(signature)) {
|
|
142
|
+
migrations.push({
|
|
143
|
+
kind: 'DropConstraint',
|
|
144
|
+
modelName: foreignKey.sourceModel,
|
|
145
|
+
constraintType: 'foreignKey',
|
|
146
|
+
details: signature,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return migrations;
|
|
151
|
+
}
|
|
152
|
+
diffIndexes(oldSchema, newSchema) {
|
|
153
|
+
const migrations = [];
|
|
154
|
+
const oldModels = new Map(oldSchema.models.map((model) => [model.name, model]));
|
|
155
|
+
const newModels = new Map(newSchema.models.map((model) => [model.name, model]));
|
|
156
|
+
const oldModelNames = getModelNames(oldSchema);
|
|
157
|
+
const newModelNames = getModelNames(newSchema);
|
|
158
|
+
for (const [modelName, newModel] of newModels) {
|
|
159
|
+
const oldModel = oldModels.get(modelName);
|
|
160
|
+
const oldIndexes = oldModel
|
|
161
|
+
? this.indexSignatures(oldModel, oldModelNames)
|
|
162
|
+
: new Set();
|
|
163
|
+
const newIndexes = this.indexSignatures(newModel, newModelNames);
|
|
164
|
+
for (const signature of newIndexes) {
|
|
165
|
+
if (!oldIndexes.has(signature)) {
|
|
166
|
+
migrations.push({ kind: 'CreateIndex', modelName, signature });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const signature of oldIndexes) {
|
|
170
|
+
if (!newIndexes.has(signature)) {
|
|
171
|
+
migrations.push({ kind: 'DropIndex', modelName, signature });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return migrations;
|
|
176
|
+
}
|
|
177
|
+
diffTriggers(oldSchema, newSchema) {
|
|
178
|
+
const migrations = [];
|
|
179
|
+
const oldModels = new Map(oldSchema.models.map((model) => [model.name, model]));
|
|
180
|
+
const newModels = new Map(newSchema.models.map((model) => [model.name, model]));
|
|
181
|
+
for (const [modelName, newModel] of newModels) {
|
|
182
|
+
const oldModel = oldModels.get(modelName);
|
|
183
|
+
const oldTriggers = oldModel ? this.triggerSignatures(oldModel) : new Set();
|
|
184
|
+
const newTriggers = this.triggerSignatures(newModel);
|
|
185
|
+
for (const signature of newTriggers) {
|
|
186
|
+
if (!oldTriggers.has(signature)) {
|
|
187
|
+
migrations.push({ kind: 'CreateTrigger', modelName, signature });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const signature of oldTriggers) {
|
|
191
|
+
if (!newTriggers.has(signature)) {
|
|
192
|
+
migrations.push({ kind: 'DropTrigger', modelName, signature });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return migrations;
|
|
197
|
+
}
|
|
198
|
+
triggerSignatures(model) {
|
|
199
|
+
return new Set(getDirectives(model, 'trigger').map((directive) => JSON.stringify(normalizeTriggerDirective(directive))));
|
|
200
|
+
}
|
|
201
|
+
indexSignatures(model, modelNames) {
|
|
202
|
+
return new Set(getDirectives(model, 'index').map((directive) => JSON.stringify(normalizeIndexDirective(directive, model, modelNames))));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function getStoredFieldNames(model, modelNames) {
|
|
206
|
+
return model.fields.filter((field) => isStoredField(field, modelNames)).map((field) => field.name);
|
|
207
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
import type { Migration } from './migration-types.js';
|
|
3
|
+
export declare class MigrationSqlGenerator {
|
|
4
|
+
generate(migrations: Migration[], newSchema: Schema): string;
|
|
5
|
+
private migrationToSql;
|
|
6
|
+
private getField;
|
|
7
|
+
private findIndexBySignature;
|
|
8
|
+
private findTriggerBySignature;
|
|
9
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { generateAddEnumValue, generateEnum } from './generators/enums.js';
|
|
2
|
+
import { generateCreateExtension, generateDropExtension } from './generators/extensions.js';
|
|
3
|
+
import { generateForeignKey } from './generators/foreign-keys.js';
|
|
4
|
+
import { generateCreateIndex, generateDropIndex, } from './generators/indexes.js';
|
|
5
|
+
import { generateColumnDefinition, generateTable } from './generators/tables.js';
|
|
6
|
+
import { generateCreateTrigger, generateDropTrigger, } from './generators/triggers.js';
|
|
7
|
+
import { getDirectives, getDefaultExpression, getEnumNames, getModelNames, getStoredFields, normalizeIndexDirective, normalizeTriggerDirective, parseForeignKeySignature, } from './utils/ast-helpers.js';
|
|
8
|
+
import { quoteIdentifier, toSnakeCase, toTableName } from './utils/snake-case.js';
|
|
9
|
+
const MIGRATION_ORDER = {
|
|
10
|
+
CreateExtension: 0,
|
|
11
|
+
CreateEnum: 1,
|
|
12
|
+
AddEnumValue: 2,
|
|
13
|
+
CreateTable: 3,
|
|
14
|
+
AddColumn: 4,
|
|
15
|
+
AlterColumn: 5,
|
|
16
|
+
AddConstraint: 6,
|
|
17
|
+
DropColumn: 7,
|
|
18
|
+
DropConstraint: 8,
|
|
19
|
+
DropIndex: 9,
|
|
20
|
+
CreateIndex: 10,
|
|
21
|
+
CreateTrigger: 11,
|
|
22
|
+
DropTrigger: 12,
|
|
23
|
+
DropTable: 13,
|
|
24
|
+
DropExtension: 14,
|
|
25
|
+
};
|
|
26
|
+
export class MigrationSqlGenerator {
|
|
27
|
+
generate(migrations, newSchema) {
|
|
28
|
+
if (migrations.length === 0) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
const enumNames = getEnumNames(newSchema);
|
|
32
|
+
const modelNames = getModelNames(newSchema);
|
|
33
|
+
const modelMap = new Map(newSchema.models.map((model) => [model.name, model]));
|
|
34
|
+
const enumMap = new Map(newSchema.enums.map((enumDef) => [enumDef.name, enumDef]));
|
|
35
|
+
const ordered = [...migrations].sort((left, right) => MIGRATION_ORDER[left.kind] - MIGRATION_ORDER[right.kind]);
|
|
36
|
+
const statements = ordered.map((migration) => this.migrationToSql(migration, { newSchema, enumNames, modelNames, modelMap, enumMap }));
|
|
37
|
+
return `${statements.join('\n\n')}\n`;
|
|
38
|
+
}
|
|
39
|
+
migrationToSql(migration, context) {
|
|
40
|
+
const { enumNames, modelNames, modelMap, enumMap } = context;
|
|
41
|
+
switch (migration.kind) {
|
|
42
|
+
case 'CreateExtension':
|
|
43
|
+
return generateCreateExtension(migration.extensionName);
|
|
44
|
+
case 'DropExtension':
|
|
45
|
+
return generateDropExtension(migration.extensionName);
|
|
46
|
+
case 'CreateEnum': {
|
|
47
|
+
const enumDef = enumMap.get(migration.enumName);
|
|
48
|
+
if (!enumDef) {
|
|
49
|
+
throw new Error(`Enum "${migration.enumName}" not found in new schema`);
|
|
50
|
+
}
|
|
51
|
+
return generateEnum(enumDef);
|
|
52
|
+
}
|
|
53
|
+
case 'AddEnumValue':
|
|
54
|
+
return generateAddEnumValue(migration.enumName, migration.value);
|
|
55
|
+
case 'CreateTable': {
|
|
56
|
+
const model = modelMap.get(migration.modelName);
|
|
57
|
+
if (!model) {
|
|
58
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
59
|
+
}
|
|
60
|
+
return generateTable(model, enumNames, modelNames);
|
|
61
|
+
}
|
|
62
|
+
case 'AddColumn': {
|
|
63
|
+
const model = modelMap.get(migration.modelName);
|
|
64
|
+
if (!model) {
|
|
65
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
66
|
+
}
|
|
67
|
+
const field = this.getField(model, migration.fieldName, modelNames);
|
|
68
|
+
const columnDef = generateColumnDefinition(field, model, enumNames, modelNames);
|
|
69
|
+
const tableName = quoteIdentifier(toTableName(model.name));
|
|
70
|
+
return `ALTER TABLE ${tableName} ADD COLUMN ${columnDef};`;
|
|
71
|
+
}
|
|
72
|
+
case 'DropColumn': {
|
|
73
|
+
const tableName = quoteIdentifier(toTableName(migration.modelName));
|
|
74
|
+
const columnName = toSnakeCase(migration.fieldName);
|
|
75
|
+
return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
|
|
76
|
+
}
|
|
77
|
+
case 'AlterColumn': {
|
|
78
|
+
const model = modelMap.get(migration.modelName);
|
|
79
|
+
if (!model) {
|
|
80
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
81
|
+
}
|
|
82
|
+
const field = this.getField(model, migration.fieldName, modelNames);
|
|
83
|
+
const tableName = quoteIdentifier(toTableName(model.name));
|
|
84
|
+
const columnName = toSnakeCase(migration.fieldName);
|
|
85
|
+
switch (migration.change.type) {
|
|
86
|
+
case 'type':
|
|
87
|
+
return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} TYPE ${migration.change.to} USING ${columnName}::${migration.change.to};`;
|
|
88
|
+
case 'nullability':
|
|
89
|
+
return migration.change.to
|
|
90
|
+
? `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} DROP NOT NULL;`
|
|
91
|
+
: `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET NOT NULL;`;
|
|
92
|
+
case 'default': {
|
|
93
|
+
const defaultExpression = getDefaultExpression(field, enumNames);
|
|
94
|
+
return defaultExpression
|
|
95
|
+
? `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET DEFAULT ${defaultExpression};`
|
|
96
|
+
: `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} DROP DEFAULT;`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
case 'DropTable': {
|
|
101
|
+
const tableName = quoteIdentifier(toTableName(migration.modelName));
|
|
102
|
+
return `DROP TABLE IF EXISTS ${tableName} CASCADE;`;
|
|
103
|
+
}
|
|
104
|
+
case 'CreateIndex': {
|
|
105
|
+
const model = modelMap.get(migration.modelName);
|
|
106
|
+
if (!model) {
|
|
107
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
108
|
+
}
|
|
109
|
+
const normalized = this.findIndexBySignature(model, migration.signature, modelNames);
|
|
110
|
+
return generateCreateIndex(model, normalized);
|
|
111
|
+
}
|
|
112
|
+
case 'DropIndex': {
|
|
113
|
+
const model = modelMap.get(migration.modelName);
|
|
114
|
+
if (!model) {
|
|
115
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
116
|
+
}
|
|
117
|
+
const normalized = JSON.parse(migration.signature);
|
|
118
|
+
return generateDropIndex(model, normalized);
|
|
119
|
+
}
|
|
120
|
+
case 'AddConstraint': {
|
|
121
|
+
if (migration.constraintType !== 'foreignKey') {
|
|
122
|
+
throw new Error(`Unsupported constraint type: ${migration.constraintType}`);
|
|
123
|
+
}
|
|
124
|
+
return generateForeignKey(parseForeignKeySignature(migration.details));
|
|
125
|
+
}
|
|
126
|
+
case 'DropConstraint': {
|
|
127
|
+
if (migration.constraintType !== 'foreignKey') {
|
|
128
|
+
throw new Error(`Unsupported constraint type: ${migration.constraintType}`);
|
|
129
|
+
}
|
|
130
|
+
const foreignKey = parseForeignKeySignature(migration.details);
|
|
131
|
+
const constraintName = `${foreignKey.sourceTable}_${foreignKey.sourceColumns.join('_')}_fkey`;
|
|
132
|
+
return `ALTER TABLE ${quoteIdentifier(foreignKey.sourceTable)} DROP CONSTRAINT ${constraintName};`;
|
|
133
|
+
}
|
|
134
|
+
case 'CreateTrigger': {
|
|
135
|
+
const model = modelMap.get(migration.modelName);
|
|
136
|
+
if (!model) {
|
|
137
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
138
|
+
}
|
|
139
|
+
const normalized = this.findTriggerBySignature(model, migration.signature);
|
|
140
|
+
return generateCreateTrigger(model, normalized);
|
|
141
|
+
}
|
|
142
|
+
case 'DropTrigger': {
|
|
143
|
+
const model = modelMap.get(migration.modelName);
|
|
144
|
+
if (!model) {
|
|
145
|
+
throw new Error(`Model "${migration.modelName}" not found in new schema`);
|
|
146
|
+
}
|
|
147
|
+
const normalized = JSON.parse(migration.signature);
|
|
148
|
+
return generateDropTrigger(model, normalized);
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
const exhaustive = migration;
|
|
152
|
+
throw new Error(`Unsupported migration kind: ${exhaustive.kind}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
getField(model, fieldName, modelNames) {
|
|
157
|
+
const field = getStoredFields(model, modelNames).find((item) => item.name === fieldName);
|
|
158
|
+
if (!field) {
|
|
159
|
+
throw new Error(`Field "${fieldName}" not found on model "${model.name}"`);
|
|
160
|
+
}
|
|
161
|
+
return field;
|
|
162
|
+
}
|
|
163
|
+
findIndexBySignature(model, signature, modelNames) {
|
|
164
|
+
for (const directive of getDirectives(model, 'index')) {
|
|
165
|
+
const normalized = normalizeIndexDirective(directive, model, modelNames);
|
|
166
|
+
if (JSON.stringify(normalized) === signature) {
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Index signature not found on model "${model.name}"`);
|
|
171
|
+
}
|
|
172
|
+
findTriggerBySignature(model, signature) {
|
|
173
|
+
for (const directive of getDirectives(model, 'trigger')) {
|
|
174
|
+
const normalized = normalizeTriggerDirective(directive);
|
|
175
|
+
if (JSON.stringify(normalized) === signature) {
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`Trigger signature not found on model "${model.name}"`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type Migration = CreateExtension | DropExtension | CreateTable | DropTable | AddColumn | DropColumn | AlterColumn | CreateIndex | DropIndex | CreateEnum | AddEnumValue | AddConstraint | DropConstraint | CreateTrigger | DropTrigger;
|
|
2
|
+
export interface CreateTable {
|
|
3
|
+
kind: 'CreateTable';
|
|
4
|
+
modelName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DropTable {
|
|
7
|
+
kind: 'DropTable';
|
|
8
|
+
modelName: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AddColumn {
|
|
11
|
+
kind: 'AddColumn';
|
|
12
|
+
modelName: string;
|
|
13
|
+
fieldName: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DropColumn {
|
|
16
|
+
kind: 'DropColumn';
|
|
17
|
+
modelName: string;
|
|
18
|
+
fieldName: string;
|
|
19
|
+
}
|
|
20
|
+
export interface AlterColumn {
|
|
21
|
+
kind: 'AlterColumn';
|
|
22
|
+
modelName: string;
|
|
23
|
+
fieldName: string;
|
|
24
|
+
change: {
|
|
25
|
+
type: 'type';
|
|
26
|
+
from: string;
|
|
27
|
+
to: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'nullability';
|
|
30
|
+
from: boolean;
|
|
31
|
+
to: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'default';
|
|
34
|
+
from?: string;
|
|
35
|
+
to?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface CreateIndex {
|
|
39
|
+
kind: 'CreateIndex';
|
|
40
|
+
modelName: string;
|
|
41
|
+
signature: string;
|
|
42
|
+
}
|
|
43
|
+
export interface DropIndex {
|
|
44
|
+
kind: 'DropIndex';
|
|
45
|
+
modelName: string;
|
|
46
|
+
signature: string;
|
|
47
|
+
}
|
|
48
|
+
export interface CreateEnum {
|
|
49
|
+
kind: 'CreateEnum';
|
|
50
|
+
enumName: string;
|
|
51
|
+
}
|
|
52
|
+
export interface AddEnumValue {
|
|
53
|
+
kind: 'AddEnumValue';
|
|
54
|
+
enumName: string;
|
|
55
|
+
value: string;
|
|
56
|
+
}
|
|
57
|
+
export interface AddConstraint {
|
|
58
|
+
kind: 'AddConstraint';
|
|
59
|
+
modelName: string;
|
|
60
|
+
constraintType: 'foreignKey' | 'primaryKey' | 'unique';
|
|
61
|
+
details: string;
|
|
62
|
+
}
|
|
63
|
+
export interface DropConstraint {
|
|
64
|
+
kind: 'DropConstraint';
|
|
65
|
+
modelName: string;
|
|
66
|
+
constraintType: 'foreignKey' | 'primaryKey' | 'unique';
|
|
67
|
+
details: string;
|
|
68
|
+
}
|
|
69
|
+
export interface CreateExtension {
|
|
70
|
+
kind: 'CreateExtension';
|
|
71
|
+
extensionName: string;
|
|
72
|
+
}
|
|
73
|
+
export interface DropExtension {
|
|
74
|
+
kind: 'DropExtension';
|
|
75
|
+
extensionName: string;
|
|
76
|
+
}
|
|
77
|
+
export interface CreateTrigger {
|
|
78
|
+
kind: 'CreateTrigger';
|
|
79
|
+
modelName: string;
|
|
80
|
+
signature: string;
|
|
81
|
+
}
|
|
82
|
+
export interface DropTrigger {
|
|
83
|
+
kind: 'DropTrigger';
|
|
84
|
+
modelName: string;
|
|
85
|
+
signature: string;
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parse } from '../schema-dsl/index.js';
|
|
2
|
+
import { generateDropTables } from './generators/drop-tables.js';
|
|
3
|
+
import { generateEnums } from './generators/enums.js';
|
|
4
|
+
import { generateExtensions } from './generators/extensions.js';
|
|
5
|
+
import { generateForeignKeys } from './generators/foreign-keys.js';
|
|
6
|
+
import { generateIndexes } from './generators/indexes.js';
|
|
7
|
+
import { generateTables } from './generators/tables.js';
|
|
8
|
+
import { generateTriggers } from './generators/triggers.js';
|
|
9
|
+
export class SqlGenerator {
|
|
10
|
+
generate(schema) {
|
|
11
|
+
const sections = [
|
|
12
|
+
generateExtensions(schema),
|
|
13
|
+
generateEnums(schema),
|
|
14
|
+
generateDropTables(schema),
|
|
15
|
+
generateTables(schema),
|
|
16
|
+
generateForeignKeys(schema),
|
|
17
|
+
generateIndexes(schema),
|
|
18
|
+
generateTriggers(schema),
|
|
19
|
+
];
|
|
20
|
+
return `${sections.join('\n')}\n`;
|
|
21
|
+
}
|
|
22
|
+
generateFromSource(source) {
|
|
23
|
+
return this.generate(parse(source));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export { toSnakeCase, toTableName } from './utils/snake-case.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Attribute, AttributeArgs, Directive, Field, KeyValueArgs, Model, Schema, TypeExpr, Value } from '../../schema-dsl/ast.js';
|
|
2
|
+
export interface PrimaryKeyInfo {
|
|
3
|
+
fields: string[];
|
|
4
|
+
composite: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ForeignKeyInfo {
|
|
7
|
+
sourceModel: string;
|
|
8
|
+
sourceTable: string;
|
|
9
|
+
sourceColumns: string[];
|
|
10
|
+
targetModel: string;
|
|
11
|
+
targetTable: string;
|
|
12
|
+
targetColumns: string[];
|
|
13
|
+
onDelete?: string;
|
|
14
|
+
onUpdate?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function getModelNames(schema: Schema): Set<string>;
|
|
17
|
+
export declare function getEnumNames(schema: Schema): Set<string>;
|
|
18
|
+
export declare function isStoredField(field: Field, modelNames: Set<string>): boolean;
|
|
19
|
+
export declare function getStoredFields(model: Model, modelNames: Set<string>): Field[];
|
|
20
|
+
export declare function getFieldAttribute(field: Field, name: string): Attribute | undefined;
|
|
21
|
+
export declare function getModelAttribute(model: Model, name: string): Attribute | undefined;
|
|
22
|
+
export declare function getDirective(model: Model, name: string): Directive | undefined;
|
|
23
|
+
export declare function getDirectives(model: Model, name: string): Directive[];
|
|
24
|
+
export declare function assertKeyValueArgs(args: AttributeArgs | undefined): KeyValueArgs;
|
|
25
|
+
export declare function getKvPair(args: KeyValueArgs, key: string): import("../../schema-dsl/ast.js").KeyValuePair;
|
|
26
|
+
export declare function getOptionalKvPair(args: KeyValueArgs, key: string): import("../../schema-dsl/ast.js").KeyValuePair | undefined;
|
|
27
|
+
export declare function getIdentifierNames(value: Value): string[];
|
|
28
|
+
export declare function fieldHasAttribute(field: Field, name: string): boolean;
|
|
29
|
+
export declare function getPrimaryKey(model: Model): PrimaryKeyInfo | undefined;
|
|
30
|
+
export declare function getDefaultExpression(field: Field, enumNames: Set<string>): string | undefined;
|
|
31
|
+
export declare function collectValidationComments(field: Field): string[];
|
|
32
|
+
export declare function mapReferentialAction(value: Value | undefined): string | undefined;
|
|
33
|
+
export declare function collectForeignKeys(schema: Schema): ForeignKeyInfo[];
|
|
34
|
+
export declare function serializeColumnType(type: TypeExpr, enumNames: Set<string>): string;
|
|
35
|
+
export declare function serializeDefault(field: Field, enumNames: Set<string>): string | undefined;
|
|
36
|
+
export declare function getFieldSnakeNameMap(model: Model, modelNames: Set<string>): Map<string, string>;
|
|
37
|
+
export declare function transformWhereClause(where: string, fieldNameMap: Map<string, string>): string;
|
|
38
|
+
export declare function serializeForeignKey(foreignKey: ForeignKeyInfo): string;
|
|
39
|
+
export declare function parseForeignKeySignature(signature: string): ForeignKeyInfo;
|
|
40
|
+
export declare function normalizeIndexDirective(directive: Directive, model: Model, modelNames: Set<string>): {
|
|
41
|
+
fields: string[];
|
|
42
|
+
where?: string;
|
|
43
|
+
unique?: boolean;
|
|
44
|
+
name?: string;
|
|
45
|
+
type?: string;
|
|
46
|
+
};
|
|
47
|
+
export interface NormalizedTrigger {
|
|
48
|
+
timing: string;
|
|
49
|
+
event: string;
|
|
50
|
+
level: string;
|
|
51
|
+
execute: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TriggerNames {
|
|
54
|
+
functionName: string;
|
|
55
|
+
triggerName: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function normalizeTriggerDirective(directive: Directive): NormalizedTrigger;
|
|
58
|
+
export declare function resolveTriggerNames(model: Model, timing: string, event: string): TriggerNames;
|