@smartive/graphql-magic 23.4.1 → 23.5.0-next.1
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/CHANGELOG.md +17 -2
- package/dist/bin/gqm.cjs +656 -59
- package/dist/cjs/index.cjs +2700 -2133
- package/dist/esm/migrations/generate-functions.d.ts +2 -0
- package/dist/esm/migrations/generate-functions.js +60 -0
- package/dist/esm/migrations/generate-functions.js.map +1 -0
- package/dist/esm/migrations/generate.d.ts +9 -1
- package/dist/esm/migrations/generate.js +269 -33
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/index.d.ts +2 -0
- package/dist/esm/migrations/index.js +2 -0
- package/dist/esm/migrations/index.js.map +1 -1
- package/dist/esm/migrations/types.d.ts +7 -0
- package/dist/esm/migrations/types.js +2 -0
- package/dist/esm/migrations/types.js.map +1 -0
- package/dist/esm/migrations/update-functions.d.ts +3 -0
- package/dist/esm/migrations/update-functions.js +177 -0
- package/dist/esm/migrations/update-functions.js.map +1 -0
- package/dist/esm/models/model-definitions.d.ts +4 -1
- package/dist/esm/resolvers/filters.js +76 -14
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/selects.js +20 -2
- package/dist/esm/resolvers/selects.js.map +1 -1
- package/dist/esm/resolvers/utils.d.ts +1 -0
- package/dist/esm/resolvers/utils.js +29 -0
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/docs/docs/3-fields.md +149 -0
- package/docs/docs/5-migrations.md +9 -1
- package/package.json +2 -2
- package/src/bin/gqm/gqm.ts +44 -5
- package/src/bin/gqm/parse-functions.ts +141 -0
- package/src/bin/gqm/settings.ts +7 -0
- package/src/bin/gqm/utils.ts +1 -0
- package/src/migrations/generate-functions.ts +74 -0
- package/src/migrations/generate.ts +334 -41
- package/src/migrations/index.ts +2 -0
- package/src/migrations/types.ts +7 -0
- package/src/migrations/update-functions.ts +221 -0
- package/src/models/model-definitions.ts +4 -1
- package/src/resolvers/filters.ts +88 -25
- package/src/resolvers/selects.ts +22 -5
- package/src/resolvers/utils.ts +44 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const generateFunctionsFromDatabase = async (knex) => {
|
|
2
|
+
const regularFunctions = await knex.raw(`
|
|
3
|
+
SELECT
|
|
4
|
+
pg_get_functiondef(p.oid) as definition
|
|
5
|
+
FROM pg_proc p
|
|
6
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
7
|
+
WHERE n.nspname = 'public'
|
|
8
|
+
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
9
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
10
|
+
`);
|
|
11
|
+
const aggregateFunctions = await knex.raw(`
|
|
12
|
+
SELECT
|
|
13
|
+
p.proname as name,
|
|
14
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
15
|
+
a.aggtransfn::regproc::text as trans_func,
|
|
16
|
+
a.aggfinalfn::regproc::text as final_func,
|
|
17
|
+
a.agginitval as init_val,
|
|
18
|
+
pg_catalog.format_type(a.aggtranstype, NULL) as state_type
|
|
19
|
+
FROM pg_proc p
|
|
20
|
+
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
21
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
22
|
+
WHERE n.nspname = 'public'
|
|
23
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
24
|
+
`);
|
|
25
|
+
const functions = [];
|
|
26
|
+
for (const row of regularFunctions.rows || []) {
|
|
27
|
+
if (row.definition) {
|
|
28
|
+
functions.push(row.definition.trim());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const row of aggregateFunctions.rows || []) {
|
|
32
|
+
const name = row.name || '';
|
|
33
|
+
const argumentsStr = row.arguments || '';
|
|
34
|
+
const transFunc = row.trans_func || '';
|
|
35
|
+
const finalFunc = row.final_func || '';
|
|
36
|
+
const initVal = row.init_val;
|
|
37
|
+
const stateType = row.state_type || '';
|
|
38
|
+
if (!name || !transFunc || !stateType) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let aggregateDef = `CREATE AGGREGATE ${name}(${argumentsStr}) (\n`;
|
|
42
|
+
aggregateDef += ` SFUNC = ${transFunc},\n`;
|
|
43
|
+
aggregateDef += ` STYPE = ${stateType}`;
|
|
44
|
+
if (finalFunc) {
|
|
45
|
+
aggregateDef += `,\n FINALFUNC = ${finalFunc}`;
|
|
46
|
+
}
|
|
47
|
+
if (initVal !== null && initVal !== undefined) {
|
|
48
|
+
const initValStr = typeof initVal === 'string' ? `'${initVal}'` : String(initVal);
|
|
49
|
+
aggregateDef += `,\n INITCOND = ${initValStr}`;
|
|
50
|
+
}
|
|
51
|
+
aggregateDef += '\n);';
|
|
52
|
+
functions.push(aggregateDef);
|
|
53
|
+
}
|
|
54
|
+
if (functions.length === 0) {
|
|
55
|
+
return `export const functions: string[] = [];\n`;
|
|
56
|
+
}
|
|
57
|
+
const functionsArrayString = functions.map((func) => ` ${JSON.stringify(func)}`).join(',\n');
|
|
58
|
+
return `export const functions: string[] = [\n${functionsArrayString},\n];\n`;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=generate-functions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-functions.js","sourceRoot":"","sources":["../../../src/migrations/generate-functions.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,EAAE,IAAU,EAAmB,EAAE;IACjF,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC;;;;;;;;GAQvC,CAAC,CAAC;IAEH,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;;;;GAazC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,KAAK,MAAM,GAAG,IAAI,gBAAgB,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;QAC9C,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;YACnB,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,kBAAkB,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,YAAY,GAAG,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC7B,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC;QAEvC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,SAAS;QACX,CAAC;QAED,IAAI,YAAY,GAAG,oBAAoB,IAAI,IAAI,YAAY,OAAO,CAAC;QACnE,YAAY,IAAI,aAAa,SAAS,KAAK,CAAC;QAC5C,YAAY,IAAI,aAAa,SAAS,EAAE,CAAC;QAEzC,IAAI,SAAS,EAAE,CAAC;YACd,YAAY,IAAI,oBAAoB,SAAS,EAAE,CAAC;QAClD,CAAC;QAED,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC9C,MAAM,UAAU,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClF,YAAY,IAAI,mBAAmB,UAAU,EAAE,CAAC;QAClD,CAAC;QAED,YAAY,IAAI,MAAM,CAAC;QAEvB,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,0CAA0C,CAAC;IACpD,CAAC;IAED,MAAM,oBAAoB,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAE9F,OAAO,yCAAyC,oBAAoB,SAAS,CAAC;AAChF,CAAC,CAAC"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { Knex } from 'knex';
|
|
2
2
|
import { Models } from '../models/models';
|
|
3
|
+
import { ParsedFunction } from './types';
|
|
3
4
|
export declare class MigrationGenerator {
|
|
4
5
|
private models;
|
|
6
|
+
private parsedFunctions?;
|
|
5
7
|
private writer;
|
|
6
8
|
private schema;
|
|
7
9
|
private columns;
|
|
8
10
|
private uuidUsed?;
|
|
9
11
|
private nowUsed?;
|
|
10
12
|
needsMigration: boolean;
|
|
11
|
-
|
|
13
|
+
private knex;
|
|
14
|
+
constructor(knex: Knex, models: Models, parsedFunctions?: ParsedFunction[] | undefined);
|
|
12
15
|
generate(): Promise<string>;
|
|
13
16
|
private renameFields;
|
|
14
17
|
private createFields;
|
|
@@ -30,5 +33,10 @@ export declare class MigrationGenerator {
|
|
|
30
33
|
private column;
|
|
31
34
|
private getColumn;
|
|
32
35
|
private hasChanged;
|
|
36
|
+
private normalizeFunctionBody;
|
|
37
|
+
private normalizeAggregateDefinition;
|
|
38
|
+
private extractFunctionBody;
|
|
39
|
+
private getDatabaseFunctions;
|
|
40
|
+
private handleFunctions;
|
|
33
41
|
}
|
|
34
42
|
export declare const getMigrationDate: () => string;
|
|
@@ -2,8 +2,10 @@ import CodeBlockWriter from 'code-block-writer';
|
|
|
2
2
|
import { SchemaInspector } from 'knex-schema-inspector';
|
|
3
3
|
import lowerFirst from 'lodash/lowerFirst';
|
|
4
4
|
import { and, get, isCreatableModel, isInherited, isUpdatableField, isUpdatableModel, modelNeedsTable, not, summonByName, typeToField, } from '../models/utils';
|
|
5
|
+
import { getColumnName } from '../resolvers';
|
|
5
6
|
export class MigrationGenerator {
|
|
6
7
|
models;
|
|
8
|
+
parsedFunctions;
|
|
7
9
|
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
8
10
|
writer = new CodeBlockWriter['default']({
|
|
9
11
|
useSingleQuote: true,
|
|
@@ -14,8 +16,11 @@ export class MigrationGenerator {
|
|
|
14
16
|
uuidUsed;
|
|
15
17
|
nowUsed;
|
|
16
18
|
needsMigration = false;
|
|
17
|
-
|
|
19
|
+
knex;
|
|
20
|
+
constructor(knex, models, parsedFunctions) {
|
|
18
21
|
this.models = models;
|
|
22
|
+
this.parsedFunctions = parsedFunctions;
|
|
23
|
+
this.knex = knex;
|
|
19
24
|
this.schema = SchemaInspector(knex);
|
|
20
25
|
}
|
|
21
26
|
async generate() {
|
|
@@ -28,6 +33,7 @@ export class MigrationGenerator {
|
|
|
28
33
|
const up = [];
|
|
29
34
|
const down = [];
|
|
30
35
|
this.createEnums(this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))), up, down);
|
|
36
|
+
await this.handleFunctions(up, down);
|
|
31
37
|
for (const model of models.entities) {
|
|
32
38
|
if (model.deleted) {
|
|
33
39
|
up.push(() => {
|
|
@@ -92,7 +98,9 @@ export class MigrationGenerator {
|
|
|
92
98
|
foreignKey: 'id',
|
|
93
99
|
});
|
|
94
100
|
}
|
|
95
|
-
for (const field of model.fields
|
|
101
|
+
for (const field of model.fields
|
|
102
|
+
.filter(not(isInherited))
|
|
103
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
96
104
|
this.column(field);
|
|
97
105
|
}
|
|
98
106
|
});
|
|
@@ -103,22 +111,24 @@ export class MigrationGenerator {
|
|
|
103
111
|
}
|
|
104
112
|
else {
|
|
105
113
|
// Rename fields
|
|
106
|
-
|
|
114
|
+
const fieldsToRename = model.fields.filter(not(isInherited)).filter(({ oldName }) => oldName);
|
|
115
|
+
this.renameFields(model.name, fieldsToRename, up, down);
|
|
107
116
|
// Add missing fields
|
|
108
117
|
this.createFields(model, model.fields
|
|
109
118
|
.filter(not(isInherited))
|
|
110
119
|
.filter(({ name, ...field }) => field.kind !== 'custom' &&
|
|
120
|
+
!(field.generateAs?.type === 'expression') &&
|
|
111
121
|
!this.getColumn(model.name, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)), up, down);
|
|
112
122
|
// Update fields
|
|
113
123
|
const rawExistingFields = model.fields.filter((field) => {
|
|
114
|
-
if (!field.generateAs) {
|
|
124
|
+
if (!field.generateAs || field.generateAs.type === 'expression') {
|
|
115
125
|
return false;
|
|
116
126
|
}
|
|
117
127
|
const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
|
|
118
128
|
if (!col) {
|
|
119
129
|
return false;
|
|
120
130
|
}
|
|
121
|
-
if (col.generation_expression !== field.generateAs) {
|
|
131
|
+
if (col.generation_expression !== field.generateAs.expression) {
|
|
122
132
|
return true;
|
|
123
133
|
}
|
|
124
134
|
return this.hasChanged(model, field);
|
|
@@ -126,7 +136,7 @@ export class MigrationGenerator {
|
|
|
126
136
|
if (rawExistingFields.length) {
|
|
127
137
|
this.updateFieldsRaw(model, rawExistingFields, up, down);
|
|
128
138
|
}
|
|
129
|
-
const existingFields = model.fields.filter((field) => !field.generateAs && this.hasChanged(model, field));
|
|
139
|
+
const existingFields = model.fields.filter((field) => (!field.generateAs || field.generateAs.type === 'expression') && this.hasChanged(model, field));
|
|
130
140
|
this.updateFields(model, existingFields, up, down);
|
|
131
141
|
}
|
|
132
142
|
if (isUpdatableModel(model)) {
|
|
@@ -154,7 +164,9 @@ export class MigrationGenerator {
|
|
|
154
164
|
writer.writeLine(`deleteRootType: row.deleteRootType,`);
|
|
155
165
|
writer.writeLine(`deleteRootId: row.deleteRootId,`);
|
|
156
166
|
}
|
|
157
|
-
for (const { name, kind } of model.fields
|
|
167
|
+
for (const { name, kind } of model.fields
|
|
168
|
+
.filter(isUpdatableField)
|
|
169
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
158
170
|
const col = kind === 'relation' ? `${name}Id` : name;
|
|
159
171
|
writer.writeLine(`${col}: row.${col},`);
|
|
160
172
|
}
|
|
@@ -172,9 +184,14 @@ export class MigrationGenerator {
|
|
|
172
184
|
}
|
|
173
185
|
else {
|
|
174
186
|
const revisionTable = `${model.name}Revision`;
|
|
187
|
+
this.renameFields(revisionTable, model.fields
|
|
188
|
+
.filter(isUpdatableField)
|
|
189
|
+
.filter(not(isInherited))
|
|
190
|
+
.filter(({ oldName }) => oldName), up, down);
|
|
175
191
|
const missingRevisionFields = model.fields
|
|
176
192
|
.filter(isUpdatableField)
|
|
177
193
|
.filter(({ name, ...field }) => field.kind !== 'custom' &&
|
|
194
|
+
!(field.generateAs?.type === 'expression') &&
|
|
178
195
|
!this.getColumn(revisionTable, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name));
|
|
179
196
|
this.createRevisionFields(model, missingRevisionFields, up, down);
|
|
180
197
|
const revisionFieldsToRemove = model.fields.filter(({ name, updatable, generated, ...field }) => !generated &&
|
|
@@ -233,26 +250,26 @@ export class MigrationGenerator {
|
|
|
233
250
|
this.migration('down', down.reverse());
|
|
234
251
|
return writer.toString();
|
|
235
252
|
}
|
|
236
|
-
renameFields(
|
|
253
|
+
renameFields(tableName, fields, up, down) {
|
|
237
254
|
if (!fields.length) {
|
|
238
255
|
return;
|
|
239
256
|
}
|
|
240
257
|
up.push(() => {
|
|
241
258
|
for (const field of fields) {
|
|
242
|
-
this.alterTable(
|
|
259
|
+
this.alterTable(tableName, () => {
|
|
243
260
|
this.renameColumn(field.kind === 'relation' ? `${field.oldName}Id` : get(field, 'oldName'), field.kind === 'relation' ? `${field.name}Id` : field.name);
|
|
244
261
|
});
|
|
245
262
|
}
|
|
246
263
|
});
|
|
247
264
|
down.push(() => {
|
|
248
265
|
for (const field of fields) {
|
|
249
|
-
this.alterTable(
|
|
266
|
+
this.alterTable(tableName, () => {
|
|
250
267
|
this.renameColumn(field.kind === 'relation' ? `${field.name}Id` : field.name, field.kind === 'relation' ? `${field.oldName}Id` : get(field, 'oldName'));
|
|
251
268
|
});
|
|
252
269
|
}
|
|
253
270
|
});
|
|
254
271
|
for (const field of fields) {
|
|
255
|
-
summonByName(this.columns[
|
|
272
|
+
summonByName(this.columns[tableName], field.kind === 'relation' ? `${field.oldName}Id` : field.oldName).name =
|
|
256
273
|
field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
257
274
|
}
|
|
258
275
|
}
|
|
@@ -265,6 +282,9 @@ export class MigrationGenerator {
|
|
|
265
282
|
const updates = [];
|
|
266
283
|
const postAlter = [];
|
|
267
284
|
for (const field of fields) {
|
|
285
|
+
if (field.generateAs?.type === 'expression') {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
268
288
|
alter.push(() => this.column(field, { setNonNull: field.defaultValue !== undefined }));
|
|
269
289
|
if (field.generateAs) {
|
|
270
290
|
continue;
|
|
@@ -323,7 +343,7 @@ export class MigrationGenerator {
|
|
|
323
343
|
});
|
|
324
344
|
});
|
|
325
345
|
if (isUpdatableModel(model)) {
|
|
326
|
-
const updatableFields = fields.filter(isUpdatableField);
|
|
346
|
+
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
|
|
327
347
|
if (!updatableFields.length) {
|
|
328
348
|
return;
|
|
329
349
|
}
|
|
@@ -362,7 +382,7 @@ export class MigrationGenerator {
|
|
|
362
382
|
});
|
|
363
383
|
});
|
|
364
384
|
if (isUpdatableModel(model)) {
|
|
365
|
-
const updatableFields = fields.filter(isUpdatableField);
|
|
385
|
+
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
|
|
366
386
|
if (!updatableFields.length) {
|
|
367
387
|
return;
|
|
368
388
|
}
|
|
@@ -397,7 +417,9 @@ export class MigrationGenerator {
|
|
|
397
417
|
writer.writeLine(`table.uuid('deleteRootId');`);
|
|
398
418
|
}
|
|
399
419
|
}
|
|
400
|
-
for (const field of model.fields
|
|
420
|
+
for (const field of model.fields
|
|
421
|
+
.filter(and(isUpdatableField, not(isInherited)))
|
|
422
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
401
423
|
this.column(field, { setUnique: false, setDefault: false });
|
|
402
424
|
}
|
|
403
425
|
});
|
|
@@ -413,19 +435,24 @@ export class MigrationGenerator {
|
|
|
413
435
|
}
|
|
414
436
|
});
|
|
415
437
|
// Insert data for missing revisions columns
|
|
416
|
-
this.
|
|
417
|
-
.
|
|
418
|
-
.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
438
|
+
const revisionFieldsWithDataToCopy = missingRevisionFields.filter((field) => this.columns[model.name].find((col) => col.name === getColumnName(field)) ||
|
|
439
|
+
field.defaultValue !== undefined ||
|
|
440
|
+
field.nonNull);
|
|
441
|
+
if (revisionFieldsWithDataToCopy.length) {
|
|
442
|
+
this.writer
|
|
443
|
+
.write(`await knex('${model.name}Revision').update(`)
|
|
444
|
+
.inlineBlock(() => {
|
|
445
|
+
for (const { name, kind: type } of revisionFieldsWithDataToCopy) {
|
|
446
|
+
const col = type === 'relation' ? `${name}Id` : name;
|
|
447
|
+
this.writer
|
|
448
|
+
.write(`${col}: knex.raw('(select "${col}" from "${model.name}" where "${model.name}".id = "${model.name}Revision"."${typeToField(model.name)}Id")'),`)
|
|
449
|
+
.newLine();
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
.write(');')
|
|
453
|
+
.newLine()
|
|
454
|
+
.blankLine();
|
|
455
|
+
}
|
|
429
456
|
const nonNullableMissingRevisionFields = missingRevisionFields.filter(({ nonNull }) => nonNull);
|
|
430
457
|
if (nonNullableMissingRevisionFields.length) {
|
|
431
458
|
this.alterTable(revisionTable, () => {
|
|
@@ -494,7 +521,7 @@ export class MigrationGenerator {
|
|
|
494
521
|
return this.writer.writeLine(`await knex.schema.renameTable('${from}', '${to}');`).blankLine();
|
|
495
522
|
}
|
|
496
523
|
renameColumn(from, to) {
|
|
497
|
-
this.writer.writeLine(`table.renameColumn('${from}', '${to}')
|
|
524
|
+
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
498
525
|
}
|
|
499
526
|
value(value) {
|
|
500
527
|
if (typeof value === 'string') {
|
|
@@ -519,6 +546,9 @@ export class MigrationGenerator {
|
|
|
519
546
|
};
|
|
520
547
|
const kind = field.kind;
|
|
521
548
|
if (field.generateAs) {
|
|
549
|
+
if (field.generateAs.type === 'expression') {
|
|
550
|
+
throw new Error(`Expression fields cannot be created in SQL schema.`);
|
|
551
|
+
}
|
|
522
552
|
let type = '';
|
|
523
553
|
switch (kind) {
|
|
524
554
|
case undefined:
|
|
@@ -527,6 +557,9 @@ export class MigrationGenerator {
|
|
|
527
557
|
case 'Float':
|
|
528
558
|
type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
|
|
529
559
|
break;
|
|
560
|
+
case 'Boolean':
|
|
561
|
+
type = 'boolean';
|
|
562
|
+
break;
|
|
530
563
|
default:
|
|
531
564
|
throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
|
|
532
565
|
}
|
|
@@ -547,10 +580,10 @@ export class MigrationGenerator {
|
|
|
547
580
|
this.writer.write(`, ALTER COLUMN "${name}" DROP NOT NULL`);
|
|
548
581
|
}
|
|
549
582
|
}
|
|
550
|
-
this.writer.write(`, ALTER COLUMN "${name}" SET EXPRESSION AS (${field.generateAs})`);
|
|
583
|
+
this.writer.write(`, ALTER COLUMN "${name}" SET EXPRESSION AS (${field.generateAs.expression})`);
|
|
551
584
|
}
|
|
552
585
|
else {
|
|
553
|
-
this.writer.write(`${alter ? 'ALTER' : 'ADD'} COLUMN "${name}" ${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED`);
|
|
586
|
+
this.writer.write(`${alter ? 'ALTER' : 'ADD'} COLUMN "${name}" ${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs.expression}) STORED`);
|
|
554
587
|
}
|
|
555
588
|
return;
|
|
556
589
|
}
|
|
@@ -573,6 +606,9 @@ export class MigrationGenerator {
|
|
|
573
606
|
};
|
|
574
607
|
const kind = field.kind;
|
|
575
608
|
if (field.generateAs) {
|
|
609
|
+
if (field.generateAs.type === 'expression') {
|
|
610
|
+
throw new Error(`Expression fields cannot be created in SQL schema.`);
|
|
611
|
+
}
|
|
576
612
|
let type = '';
|
|
577
613
|
switch (kind) {
|
|
578
614
|
case undefined:
|
|
@@ -581,6 +617,9 @@ export class MigrationGenerator {
|
|
|
581
617
|
case 'Float':
|
|
582
618
|
type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
|
|
583
619
|
break;
|
|
620
|
+
case 'Boolean':
|
|
621
|
+
type = 'boolean';
|
|
622
|
+
break;
|
|
584
623
|
default:
|
|
585
624
|
throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
|
|
586
625
|
}
|
|
@@ -588,7 +627,7 @@ export class MigrationGenerator {
|
|
|
588
627
|
default:
|
|
589
628
|
throw new Error(`Generated columns of kind ${kind} are not supported yet.`);
|
|
590
629
|
}
|
|
591
|
-
this.writer.write(`table.specificType('${name}', '${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED')`);
|
|
630
|
+
this.writer.write(`table.specificType('${name}', '${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs.expression}) ${field.generateAs.type === 'virtual' ? 'VIRTUAL' : 'STORED'}')`);
|
|
592
631
|
if (alter) {
|
|
593
632
|
this.writer.write('.alter()');
|
|
594
633
|
}
|
|
@@ -690,13 +729,16 @@ export class MigrationGenerator {
|
|
|
690
729
|
return this.columns[tableName].find((col) => col.name === columnName);
|
|
691
730
|
}
|
|
692
731
|
hasChanged(model, field) {
|
|
732
|
+
if (field.generateAs?.type === 'expression') {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
693
735
|
const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
|
|
694
736
|
if (!col) {
|
|
695
737
|
return false;
|
|
696
738
|
}
|
|
697
739
|
if (field.generateAs) {
|
|
698
|
-
if (col.generation_expression !== field.generateAs) {
|
|
699
|
-
throw new Error(`Column ${col.name} has specific type ${col.generation_expression} but expected ${field.generateAs}`);
|
|
740
|
+
if (col.generation_expression !== field.generateAs.expression) {
|
|
741
|
+
throw new Error(`Column ${col.name} has specific type ${col.generation_expression} but expected ${field.generateAs.expression}`);
|
|
700
742
|
}
|
|
701
743
|
}
|
|
702
744
|
if ((!field.nonNull && !col.is_nullable) || (field.nonNull && col.is_nullable)) {
|
|
@@ -738,6 +780,200 @@ export class MigrationGenerator {
|
|
|
738
780
|
}
|
|
739
781
|
return false;
|
|
740
782
|
}
|
|
783
|
+
normalizeFunctionBody(body) {
|
|
784
|
+
return body
|
|
785
|
+
.replace(/\s+/g, ' ')
|
|
786
|
+
.replace(/\s*\(\s*/g, '(')
|
|
787
|
+
.replace(/\s*\)\s*/g, ')')
|
|
788
|
+
.replace(/\s*,\s*/g, ',')
|
|
789
|
+
.trim();
|
|
790
|
+
}
|
|
791
|
+
normalizeAggregateDefinition(definition) {
|
|
792
|
+
let normalized = definition
|
|
793
|
+
.replace(/\s+/g, ' ')
|
|
794
|
+
.replace(/\s*\(\s*/g, '(')
|
|
795
|
+
.replace(/\s*\)\s*/g, ')')
|
|
796
|
+
.replace(/\s*,\s*/g, ',')
|
|
797
|
+
.trim();
|
|
798
|
+
const initCondMatch = normalized.match(/INITCOND\s*=\s*([^,)]+)/i);
|
|
799
|
+
if (initCondMatch) {
|
|
800
|
+
const initCondValue = initCondMatch[1].trim();
|
|
801
|
+
const unquoted = initCondValue.replace(/^['"]|['"]$/g, '');
|
|
802
|
+
if (/^\d+$/.test(unquoted)) {
|
|
803
|
+
normalized = normalized.replace(/INITCOND\s*=\s*[^,)]+/i, `INITCOND = '${unquoted}'`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return normalized;
|
|
807
|
+
}
|
|
808
|
+
extractFunctionBody(definition) {
|
|
809
|
+
const dollarQuoteMatch = definition.match(/AS\s+\$([^$]*)\$([\s\S]*?)\$\1\$/i);
|
|
810
|
+
if (dollarQuoteMatch) {
|
|
811
|
+
return dollarQuoteMatch[2].trim();
|
|
812
|
+
}
|
|
813
|
+
const bodyMatch = definition.match(/AS\s+\$\$([\s\S]*?)\$\$/i) || definition.match(/AS\s+['"]([\s\S]*?)['"]/i);
|
|
814
|
+
if (bodyMatch) {
|
|
815
|
+
return bodyMatch[1].trim();
|
|
816
|
+
}
|
|
817
|
+
return definition;
|
|
818
|
+
}
|
|
819
|
+
async getDatabaseFunctions() {
|
|
820
|
+
const regularFunctions = await this.knex.raw(`
|
|
821
|
+
SELECT
|
|
822
|
+
p.proname as name,
|
|
823
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
824
|
+
pg_get_functiondef(p.oid) as definition,
|
|
825
|
+
false as is_aggregate
|
|
826
|
+
FROM pg_proc p
|
|
827
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
828
|
+
WHERE n.nspname = 'public'
|
|
829
|
+
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
830
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
831
|
+
`);
|
|
832
|
+
const aggregateFunctions = await this.knex.raw(`
|
|
833
|
+
SELECT
|
|
834
|
+
p.proname as name,
|
|
835
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
836
|
+
a.aggtransfn::regproc::text as trans_func,
|
|
837
|
+
a.aggfinalfn::regproc::text as final_func,
|
|
838
|
+
a.agginitval as init_val,
|
|
839
|
+
pg_catalog.format_type(a.aggtranstype, NULL) as state_type,
|
|
840
|
+
true as is_aggregate
|
|
841
|
+
FROM pg_proc p
|
|
842
|
+
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
843
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
844
|
+
WHERE n.nspname = 'public'
|
|
845
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
846
|
+
`);
|
|
847
|
+
const result = [];
|
|
848
|
+
for (const row of regularFunctions.rows || []) {
|
|
849
|
+
const definition = row.definition || '';
|
|
850
|
+
const name = row.name || '';
|
|
851
|
+
const argumentsStr = row.arguments || '';
|
|
852
|
+
if (!definition) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const signature = `${name}(${argumentsStr})`;
|
|
856
|
+
const body = this.normalizeFunctionBody(this.extractFunctionBody(definition));
|
|
857
|
+
result.push({
|
|
858
|
+
name,
|
|
859
|
+
signature,
|
|
860
|
+
body,
|
|
861
|
+
isAggregate: false,
|
|
862
|
+
definition,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
for (const row of aggregateFunctions.rows || []) {
|
|
866
|
+
const name = row.name || '';
|
|
867
|
+
const argumentsStr = row.arguments || '';
|
|
868
|
+
const transFunc = row.trans_func || '';
|
|
869
|
+
const finalFunc = row.final_func || '';
|
|
870
|
+
const initVal = row.init_val;
|
|
871
|
+
const stateType = row.state_type || '';
|
|
872
|
+
const signature = `${name}(${argumentsStr})`;
|
|
873
|
+
let aggregateDef = `CREATE AGGREGATE ${name}(${argumentsStr}) (`;
|
|
874
|
+
aggregateDef += `SFUNC = ${transFunc}, STYPE = ${stateType}`;
|
|
875
|
+
if (finalFunc) {
|
|
876
|
+
aggregateDef += `, FINALFUNC = ${finalFunc}`;
|
|
877
|
+
}
|
|
878
|
+
if (initVal !== null && initVal !== undefined) {
|
|
879
|
+
let initValStr;
|
|
880
|
+
if (typeof initVal === 'string') {
|
|
881
|
+
initValStr = `'${initVal}'`;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
const numStr = String(initVal);
|
|
885
|
+
initValStr = /^\d+$/.test(numStr) ? `'${numStr}'` : numStr;
|
|
886
|
+
}
|
|
887
|
+
aggregateDef += `, INITCOND = ${initValStr}`;
|
|
888
|
+
}
|
|
889
|
+
aggregateDef += ');';
|
|
890
|
+
result.push({
|
|
891
|
+
name,
|
|
892
|
+
signature,
|
|
893
|
+
body: this.normalizeAggregateDefinition(aggregateDef),
|
|
894
|
+
isAggregate: true,
|
|
895
|
+
definition: aggregateDef,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
async handleFunctions(up, down) {
|
|
901
|
+
if (!this.parsedFunctions || this.parsedFunctions.length === 0) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const definedFunctions = this.parsedFunctions;
|
|
905
|
+
const dbFunctions = await this.getDatabaseFunctions();
|
|
906
|
+
const dbFunctionsBySignature = new Map();
|
|
907
|
+
for (const func of dbFunctions) {
|
|
908
|
+
dbFunctionsBySignature.set(func.signature, func);
|
|
909
|
+
}
|
|
910
|
+
const definedFunctionsBySignature = new Map();
|
|
911
|
+
for (const func of definedFunctions) {
|
|
912
|
+
definedFunctionsBySignature.set(func.signature, func);
|
|
913
|
+
}
|
|
914
|
+
const functionsToRestore = [];
|
|
915
|
+
for (const definedFunc of definedFunctions) {
|
|
916
|
+
const dbFunc = dbFunctionsBySignature.get(definedFunc.signature);
|
|
917
|
+
if (!dbFunc) {
|
|
918
|
+
up.push(() => {
|
|
919
|
+
this.writer.writeLine(`await knex.raw(\`${definedFunc.fullDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
920
|
+
});
|
|
921
|
+
down.push(() => {
|
|
922
|
+
const isAggregate = definedFunc.isAggregate;
|
|
923
|
+
const dropMatch = definedFunc.fullDefinition.match(/CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)\s+([^(]+)\(/i);
|
|
924
|
+
if (dropMatch) {
|
|
925
|
+
const functionName = dropMatch[3].trim();
|
|
926
|
+
const argsMatch = definedFunc.fullDefinition.match(/CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)\s+[^(]+\(([^)]*)\)/i);
|
|
927
|
+
const args = argsMatch ? argsMatch[3].trim() : '';
|
|
928
|
+
const dropType = isAggregate ? 'AGGREGATE' : 'FUNCTION';
|
|
929
|
+
this.writer
|
|
930
|
+
.writeLine(`await knex.raw(\`DROP ${dropType} IF EXISTS ${functionName}${args ? `(${args})` : ''}\`);`)
|
|
931
|
+
.blankLine();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const dbBody = dbFunc.isAggregate
|
|
937
|
+
? this.normalizeAggregateDefinition(dbFunc.body)
|
|
938
|
+
: this.normalizeFunctionBody(dbFunc.body);
|
|
939
|
+
const definedBody = definedFunc.isAggregate
|
|
940
|
+
? this.normalizeAggregateDefinition(definedFunc.body)
|
|
941
|
+
: this.normalizeFunctionBody(definedFunc.body);
|
|
942
|
+
if (dbBody !== definedBody) {
|
|
943
|
+
const oldDefinition = dbFunc.definition || dbFunc.body;
|
|
944
|
+
up.push(() => {
|
|
945
|
+
this.writer.writeLine(`await knex.raw(\`${definedFunc.fullDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
946
|
+
});
|
|
947
|
+
down.push(() => {
|
|
948
|
+
if (oldDefinition) {
|
|
949
|
+
this.writer.writeLine(`await knex.raw(\`${oldDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
for (const dbFunc of dbFunctions) {
|
|
956
|
+
if (!definedFunctionsBySignature.has(dbFunc.signature)) {
|
|
957
|
+
const definition = dbFunc.definition || dbFunc.body;
|
|
958
|
+
if (definition) {
|
|
959
|
+
functionsToRestore.push({ func: dbFunc, definition });
|
|
960
|
+
down.push(() => {
|
|
961
|
+
const argsMatch = dbFunc.signature.match(/\(([^)]*)\)/);
|
|
962
|
+
const args = argsMatch ? argsMatch[1] : '';
|
|
963
|
+
const dropType = dbFunc.isAggregate ? 'AGGREGATE' : 'FUNCTION';
|
|
964
|
+
this.writer
|
|
965
|
+
.writeLine(`await knex.raw(\`DROP ${dropType} IF EXISTS ${dbFunc.name}${args ? `(${args})` : ''}\`);`)
|
|
966
|
+
.blankLine();
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (const { definition } of functionsToRestore) {
|
|
972
|
+
up.push(() => {
|
|
973
|
+
this.writer.writeLine(`await knex.raw(\`${definition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
741
977
|
}
|
|
742
978
|
export const getMigrationDate = () => {
|
|
743
979
|
const date = new Date();
|