@smartive/graphql-magic 23.3.0 → 23.4.0-next.4
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/.github/workflows/release.yml +8 -2
- package/CHANGELOG.md +4 -2
- package/dist/bin/gqm.cjs +642 -63
- package/dist/cjs/index.cjs +2767 -2107
- package/dist/esm/migrations/generate-functions.d.ts +2 -0
- package/dist/esm/migrations/generate-functions.js +59 -0
- package/dist/esm/migrations/generate-functions.js.map +1 -0
- package/dist/esm/migrations/generate.d.ts +8 -1
- package/dist/esm/migrations/generate.js +273 -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/parse-functions.d.ts +8 -0
- package/dist/esm/migrations/parse-functions.js +105 -0
- package/dist/esm/migrations/parse-functions.js.map +1 -0
- package/dist/esm/migrations/update-functions.d.ts +2 -0
- package/dist/esm/migrations/update-functions.js +174 -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 +73 -14
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/selects.js +33 -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 +22 -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 +5 -1
- package/src/bin/gqm/gqm.ts +40 -5
- package/src/bin/gqm/settings.ts +7 -0
- package/src/migrations/generate-functions.ts +72 -0
- package/src/migrations/generate.ts +338 -41
- package/src/migrations/index.ts +2 -0
- package/src/migrations/parse-functions.ts +140 -0
- package/src/migrations/update-functions.ts +216 -0
- package/src/models/model-definitions.ts +4 -1
- package/src/resolvers/filters.ts +81 -25
- package/src/resolvers/selects.ts +38 -5
- package/src/resolvers/utils.ts +32 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
export const generateFunctionsFromDatabase = async (knex: Knex): Promise<string> => {
|
|
4
|
+
const regularFunctions = await knex.raw(`
|
|
5
|
+
SELECT
|
|
6
|
+
pg_get_functiondef(p.oid) as definition
|
|
7
|
+
FROM pg_proc p
|
|
8
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
9
|
+
WHERE n.nspname = 'public'
|
|
10
|
+
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
11
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
12
|
+
`);
|
|
13
|
+
|
|
14
|
+
const aggregateFunctions = await knex.raw(`
|
|
15
|
+
SELECT
|
|
16
|
+
p.proname as name,
|
|
17
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
18
|
+
a.aggtransfn::regproc::text as trans_func,
|
|
19
|
+
a.aggfinalfn::regproc::text as final_func,
|
|
20
|
+
a.agginitval as init_val,
|
|
21
|
+
pg_catalog.format_type(a.aggtranstype, NULL) as state_type
|
|
22
|
+
FROM pg_proc p
|
|
23
|
+
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
24
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
25
|
+
WHERE n.nspname = 'public'
|
|
26
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
27
|
+
`);
|
|
28
|
+
|
|
29
|
+
const functions: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const row of regularFunctions.rows || []) {
|
|
32
|
+
if (row.definition) {
|
|
33
|
+
functions.push(row.definition.trim());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const row of aggregateFunctions.rows || []) {
|
|
38
|
+
const name = row.name || '';
|
|
39
|
+
const argumentsStr = row.arguments || '';
|
|
40
|
+
const transFunc = row.trans_func || '';
|
|
41
|
+
const finalFunc = row.final_func || '';
|
|
42
|
+
const initVal = row.init_val;
|
|
43
|
+
const stateType = row.state_type || '';
|
|
44
|
+
|
|
45
|
+
if (!name || !transFunc || !stateType) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let aggregateDef = `CREATE AGGREGATE ${name}(${argumentsStr}) (\n`;
|
|
50
|
+
aggregateDef += ` SFUNC = ${transFunc},\n`;
|
|
51
|
+
aggregateDef += ` STYPE = ${stateType}`;
|
|
52
|
+
|
|
53
|
+
if (finalFunc) {
|
|
54
|
+
aggregateDef += `,\n FINALFUNC = ${finalFunc}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (initVal !== null && initVal !== undefined) {
|
|
58
|
+
const initValStr = typeof initVal === 'string' ? `'${initVal}'` : String(initVal);
|
|
59
|
+
aggregateDef += `,\n INITCOND = ${initValStr}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
aggregateDef += '\n);';
|
|
63
|
+
|
|
64
|
+
functions.push(aggregateDef);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (functions.length === 0) {
|
|
68
|
+
return '-- PostgreSQL functions\n-- No functions found in database\n';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return functions.join('\n\n') + '\n';
|
|
72
|
+
};
|
|
@@ -17,10 +17,20 @@ import {
|
|
|
17
17
|
summonByName,
|
|
18
18
|
typeToField,
|
|
19
19
|
} from '../models/utils';
|
|
20
|
+
import { getColumnName } from '../resolvers';
|
|
20
21
|
import { Value } from '../values';
|
|
22
|
+
import { ParsedFunction, parseFunctionsFile } from './parse-functions';
|
|
21
23
|
|
|
22
24
|
type Callbacks = (() => void)[];
|
|
23
25
|
|
|
26
|
+
type DatabaseFunction = {
|
|
27
|
+
name: string;
|
|
28
|
+
signature: string;
|
|
29
|
+
body: string;
|
|
30
|
+
isAggregate: boolean;
|
|
31
|
+
definition?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
24
34
|
export class MigrationGenerator {
|
|
25
35
|
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
26
36
|
private writer: CodeBlockWriter = new CodeBlockWriter['default']({
|
|
@@ -32,11 +42,14 @@ export class MigrationGenerator {
|
|
|
32
42
|
private uuidUsed?: boolean;
|
|
33
43
|
private nowUsed?: boolean;
|
|
34
44
|
public needsMigration = false;
|
|
45
|
+
private knex: Knex;
|
|
35
46
|
|
|
36
47
|
constructor(
|
|
37
48
|
knex: Knex,
|
|
38
49
|
private models: Models,
|
|
50
|
+
private functionsFilePath?: string,
|
|
39
51
|
) {
|
|
52
|
+
this.knex = knex;
|
|
40
53
|
this.schema = SchemaInspector(knex);
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -57,6 +70,8 @@ export class MigrationGenerator {
|
|
|
57
70
|
down,
|
|
58
71
|
);
|
|
59
72
|
|
|
73
|
+
await this.handleFunctions(up, down);
|
|
74
|
+
|
|
60
75
|
for (const model of models.entities) {
|
|
61
76
|
if (model.deleted) {
|
|
62
77
|
up.push(() => {
|
|
@@ -127,7 +142,9 @@ export class MigrationGenerator {
|
|
|
127
142
|
foreignKey: 'id',
|
|
128
143
|
});
|
|
129
144
|
}
|
|
130
|
-
for (const field of model.fields
|
|
145
|
+
for (const field of model.fields
|
|
146
|
+
.filter(not(isInherited))
|
|
147
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
131
148
|
this.column(field);
|
|
132
149
|
}
|
|
133
150
|
});
|
|
@@ -138,12 +155,8 @@ export class MigrationGenerator {
|
|
|
138
155
|
});
|
|
139
156
|
} else {
|
|
140
157
|
// Rename fields
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
model.fields.filter(not(isInherited)).filter(({ oldName }) => oldName),
|
|
144
|
-
up,
|
|
145
|
-
down,
|
|
146
|
-
);
|
|
158
|
+
const fieldsToRename = model.fields.filter(not(isInherited)).filter(({ oldName }) => oldName);
|
|
159
|
+
this.renameFields(model.name, fieldsToRename, up, down);
|
|
147
160
|
|
|
148
161
|
// Add missing fields
|
|
149
162
|
this.createFields(
|
|
@@ -153,6 +166,7 @@ export class MigrationGenerator {
|
|
|
153
166
|
.filter(
|
|
154
167
|
({ name, ...field }) =>
|
|
155
168
|
field.kind !== 'custom' &&
|
|
169
|
+
!(field.generateAs?.type === 'expression') &&
|
|
156
170
|
!this.getColumn(model.name, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name),
|
|
157
171
|
),
|
|
158
172
|
up,
|
|
@@ -161,7 +175,7 @@ export class MigrationGenerator {
|
|
|
161
175
|
|
|
162
176
|
// Update fields
|
|
163
177
|
const rawExistingFields = model.fields.filter((field) => {
|
|
164
|
-
if (!field.generateAs) {
|
|
178
|
+
if (!field.generateAs || field.generateAs.type === 'expression') {
|
|
165
179
|
return false;
|
|
166
180
|
}
|
|
167
181
|
|
|
@@ -170,7 +184,7 @@ export class MigrationGenerator {
|
|
|
170
184
|
return false;
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
if (col.generation_expression !== field.generateAs) {
|
|
187
|
+
if (col.generation_expression !== field.generateAs.expression) {
|
|
174
188
|
return true;
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -180,7 +194,9 @@ export class MigrationGenerator {
|
|
|
180
194
|
this.updateFieldsRaw(model, rawExistingFields, up, down);
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
const existingFields = model.fields.filter(
|
|
197
|
+
const existingFields = model.fields.filter(
|
|
198
|
+
(field) => (!field.generateAs || field.generateAs.type === 'expression') && this.hasChanged(model, field),
|
|
199
|
+
);
|
|
184
200
|
this.updateFields(model, existingFields, up, down);
|
|
185
201
|
}
|
|
186
202
|
|
|
@@ -212,7 +228,9 @@ export class MigrationGenerator {
|
|
|
212
228
|
writer.writeLine(`deleteRootId: row.deleteRootId,`);
|
|
213
229
|
}
|
|
214
230
|
|
|
215
|
-
for (const { name, kind } of model.fields
|
|
231
|
+
for (const { name, kind } of model.fields
|
|
232
|
+
.filter(isUpdatableField)
|
|
233
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
216
234
|
const col = kind === 'relation' ? `${name}Id` : name;
|
|
217
235
|
|
|
218
236
|
writer.writeLine(`${col}: row.${col},`);
|
|
@@ -231,11 +249,23 @@ export class MigrationGenerator {
|
|
|
231
249
|
});
|
|
232
250
|
} else {
|
|
233
251
|
const revisionTable = `${model.name}Revision`;
|
|
252
|
+
|
|
253
|
+
this.renameFields(
|
|
254
|
+
revisionTable,
|
|
255
|
+
model.fields
|
|
256
|
+
.filter(isUpdatableField)
|
|
257
|
+
.filter(not(isInherited))
|
|
258
|
+
.filter(({ oldName }) => oldName),
|
|
259
|
+
up,
|
|
260
|
+
down,
|
|
261
|
+
);
|
|
262
|
+
|
|
234
263
|
const missingRevisionFields = model.fields
|
|
235
264
|
.filter(isUpdatableField)
|
|
236
265
|
.filter(
|
|
237
266
|
({ name, ...field }) =>
|
|
238
267
|
field.kind !== 'custom' &&
|
|
268
|
+
!(field.generateAs?.type === 'expression') &&
|
|
239
269
|
!this.getColumn(revisionTable, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name),
|
|
240
270
|
);
|
|
241
271
|
|
|
@@ -322,14 +352,14 @@ export class MigrationGenerator {
|
|
|
322
352
|
return writer.toString();
|
|
323
353
|
}
|
|
324
354
|
|
|
325
|
-
private renameFields(
|
|
355
|
+
private renameFields(tableName: string, fields: EntityField[], up: Callbacks, down: Callbacks) {
|
|
326
356
|
if (!fields.length) {
|
|
327
357
|
return;
|
|
328
358
|
}
|
|
329
359
|
|
|
330
360
|
up.push(() => {
|
|
331
361
|
for (const field of fields) {
|
|
332
|
-
this.alterTable(
|
|
362
|
+
this.alterTable(tableName, () => {
|
|
333
363
|
this.renameColumn(
|
|
334
364
|
field.kind === 'relation' ? `${field.oldName}Id` : get(field, 'oldName'),
|
|
335
365
|
field.kind === 'relation' ? `${field.name}Id` : field.name,
|
|
@@ -340,7 +370,7 @@ export class MigrationGenerator {
|
|
|
340
370
|
|
|
341
371
|
down.push(() => {
|
|
342
372
|
for (const field of fields) {
|
|
343
|
-
this.alterTable(
|
|
373
|
+
this.alterTable(tableName, () => {
|
|
344
374
|
this.renameColumn(
|
|
345
375
|
field.kind === 'relation' ? `${field.name}Id` : field.name,
|
|
346
376
|
field.kind === 'relation' ? `${field.oldName}Id` : get(field, 'oldName'),
|
|
@@ -350,7 +380,7 @@ export class MigrationGenerator {
|
|
|
350
380
|
});
|
|
351
381
|
|
|
352
382
|
for (const field of fields) {
|
|
353
|
-
summonByName(this.columns[
|
|
383
|
+
summonByName(this.columns[tableName], field.kind === 'relation' ? `${field.oldName!}Id` : field.oldName!).name =
|
|
354
384
|
field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
355
385
|
}
|
|
356
386
|
}
|
|
@@ -365,6 +395,10 @@ export class MigrationGenerator {
|
|
|
365
395
|
const updates: Callbacks = [];
|
|
366
396
|
const postAlter: Callbacks = [];
|
|
367
397
|
for (const field of fields) {
|
|
398
|
+
if (field.generateAs?.type === 'expression') {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
368
402
|
alter.push(() => this.column(field, { setNonNull: field.defaultValue !== undefined }));
|
|
369
403
|
|
|
370
404
|
if (field.generateAs) {
|
|
@@ -430,7 +464,7 @@ export class MigrationGenerator {
|
|
|
430
464
|
});
|
|
431
465
|
|
|
432
466
|
if (isUpdatableModel(model)) {
|
|
433
|
-
const updatableFields = fields.filter(isUpdatableField);
|
|
467
|
+
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
|
|
434
468
|
if (!updatableFields.length) {
|
|
435
469
|
return;
|
|
436
470
|
}
|
|
@@ -484,7 +518,7 @@ export class MigrationGenerator {
|
|
|
484
518
|
});
|
|
485
519
|
|
|
486
520
|
if (isUpdatableModel(model)) {
|
|
487
|
-
const updatableFields = fields.filter(isUpdatableField);
|
|
521
|
+
const updatableFields = fields.filter(isUpdatableField).filter((f) => !(f.generateAs?.type === 'expression'));
|
|
488
522
|
if (!updatableFields.length) {
|
|
489
523
|
return;
|
|
490
524
|
}
|
|
@@ -528,7 +562,9 @@ export class MigrationGenerator {
|
|
|
528
562
|
}
|
|
529
563
|
}
|
|
530
564
|
|
|
531
|
-
for (const field of model.fields
|
|
565
|
+
for (const field of model.fields
|
|
566
|
+
.filter(and(isUpdatableField, not(isInherited)))
|
|
567
|
+
.filter((f) => !(f.generateAs?.type === 'expression'))) {
|
|
532
568
|
this.column(field, { setUnique: false, setDefault: false });
|
|
533
569
|
}
|
|
534
570
|
});
|
|
@@ -546,23 +582,31 @@ export class MigrationGenerator {
|
|
|
546
582
|
});
|
|
547
583
|
|
|
548
584
|
// Insert data for missing revisions columns
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
585
|
+
const revisionFieldsWithDataToCopy = missingRevisionFields.filter(
|
|
586
|
+
(field) =>
|
|
587
|
+
this.columns[model.name].find((col) => col.name === getColumnName(field)) ||
|
|
588
|
+
field.defaultValue !== undefined ||
|
|
589
|
+
field.nonNull,
|
|
590
|
+
);
|
|
591
|
+
if (revisionFieldsWithDataToCopy.length) {
|
|
592
|
+
this.writer
|
|
593
|
+
.write(`await knex('${model.name}Revision').update(`)
|
|
594
|
+
.inlineBlock(() => {
|
|
595
|
+
for (const { name, kind: type } of revisionFieldsWithDataToCopy) {
|
|
596
|
+
const col = type === 'relation' ? `${name}Id` : name;
|
|
597
|
+
this.writer
|
|
598
|
+
.write(
|
|
599
|
+
`${col}: knex.raw('(select "${col}" from "${model.name}" where "${model.name}".id = "${
|
|
600
|
+
model.name
|
|
601
|
+
}Revision"."${typeToField(model.name)}Id")'),`,
|
|
602
|
+
)
|
|
603
|
+
.newLine();
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
.write(');')
|
|
607
|
+
.newLine()
|
|
608
|
+
.blankLine();
|
|
609
|
+
}
|
|
566
610
|
|
|
567
611
|
const nonNullableMissingRevisionFields = missingRevisionFields.filter(({ nonNull }) => nonNull);
|
|
568
612
|
if (nonNullableMissingRevisionFields.length) {
|
|
@@ -646,7 +690,7 @@ export class MigrationGenerator {
|
|
|
646
690
|
}
|
|
647
691
|
|
|
648
692
|
private renameColumn(from: string, to: string) {
|
|
649
|
-
this.writer.writeLine(`table.renameColumn('${from}', '${to}')
|
|
693
|
+
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
650
694
|
}
|
|
651
695
|
|
|
652
696
|
private value(value: Value) {
|
|
@@ -681,6 +725,10 @@ export class MigrationGenerator {
|
|
|
681
725
|
};
|
|
682
726
|
const kind = field.kind;
|
|
683
727
|
if (field.generateAs) {
|
|
728
|
+
if (field.generateAs.type === 'expression') {
|
|
729
|
+
throw new Error(`Expression fields cannot be created in SQL schema.`);
|
|
730
|
+
}
|
|
731
|
+
|
|
684
732
|
let type = '';
|
|
685
733
|
switch (kind) {
|
|
686
734
|
case undefined:
|
|
@@ -689,6 +737,9 @@ export class MigrationGenerator {
|
|
|
689
737
|
case 'Float':
|
|
690
738
|
type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
|
|
691
739
|
break;
|
|
740
|
+
case 'Boolean':
|
|
741
|
+
type = 'boolean';
|
|
742
|
+
break;
|
|
692
743
|
default:
|
|
693
744
|
throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
|
|
694
745
|
}
|
|
@@ -708,10 +759,10 @@ export class MigrationGenerator {
|
|
|
708
759
|
this.writer.write(`, ALTER COLUMN "${name}" DROP NOT NULL`);
|
|
709
760
|
}
|
|
710
761
|
}
|
|
711
|
-
this.writer.write(`, ALTER COLUMN "${name}" SET EXPRESSION AS (${field.generateAs})`);
|
|
762
|
+
this.writer.write(`, ALTER COLUMN "${name}" SET EXPRESSION AS (${field.generateAs.expression})`);
|
|
712
763
|
} else {
|
|
713
764
|
this.writer.write(
|
|
714
|
-
`${alter ? 'ALTER' : 'ADD'} COLUMN "${name}" ${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED`,
|
|
765
|
+
`${alter ? 'ALTER' : 'ADD'} COLUMN "${name}" ${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs.expression}) STORED`,
|
|
715
766
|
);
|
|
716
767
|
}
|
|
717
768
|
|
|
@@ -744,6 +795,10 @@ export class MigrationGenerator {
|
|
|
744
795
|
};
|
|
745
796
|
const kind = field.kind;
|
|
746
797
|
if (field.generateAs) {
|
|
798
|
+
if (field.generateAs.type === 'expression') {
|
|
799
|
+
throw new Error(`Expression fields cannot be created in SQL schema.`);
|
|
800
|
+
}
|
|
801
|
+
|
|
747
802
|
let type = '';
|
|
748
803
|
switch (kind) {
|
|
749
804
|
case undefined:
|
|
@@ -752,6 +807,9 @@ export class MigrationGenerator {
|
|
|
752
807
|
case 'Float':
|
|
753
808
|
type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
|
|
754
809
|
break;
|
|
810
|
+
case 'Boolean':
|
|
811
|
+
type = 'boolean';
|
|
812
|
+
break;
|
|
755
813
|
default:
|
|
756
814
|
throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
|
|
757
815
|
}
|
|
@@ -760,7 +818,7 @@ export class MigrationGenerator {
|
|
|
760
818
|
throw new Error(`Generated columns of kind ${kind} are not supported yet.`);
|
|
761
819
|
}
|
|
762
820
|
this.writer.write(
|
|
763
|
-
`table.specificType('${name}', '${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED')`,
|
|
821
|
+
`table.specificType('${name}', '${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs.expression}) ${field.generateAs.type === 'virtual' ? 'VIRTUAL' : 'STORED'}')`,
|
|
764
822
|
);
|
|
765
823
|
if (alter) {
|
|
766
824
|
this.writer.write('.alter()');
|
|
@@ -865,15 +923,19 @@ export class MigrationGenerator {
|
|
|
865
923
|
}
|
|
866
924
|
|
|
867
925
|
private hasChanged(model: EntityModel, field: EntityField) {
|
|
926
|
+
if (field.generateAs?.type === 'expression') {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
|
|
868
930
|
const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
|
|
869
931
|
if (!col) {
|
|
870
932
|
return false;
|
|
871
933
|
}
|
|
872
934
|
|
|
873
935
|
if (field.generateAs) {
|
|
874
|
-
if (col.generation_expression !== field.generateAs) {
|
|
936
|
+
if (col.generation_expression !== field.generateAs.expression) {
|
|
875
937
|
throw new Error(
|
|
876
|
-
`Column ${col.name} has specific type ${col.generation_expression} but expected ${field.generateAs}`,
|
|
938
|
+
`Column ${col.name} has specific type ${col.generation_expression} but expected ${field.generateAs.expression}`,
|
|
877
939
|
);
|
|
878
940
|
}
|
|
879
941
|
}
|
|
@@ -918,6 +980,241 @@ export class MigrationGenerator {
|
|
|
918
980
|
|
|
919
981
|
return false;
|
|
920
982
|
}
|
|
983
|
+
|
|
984
|
+
private normalizeFunctionBody(body: string): string {
|
|
985
|
+
return body
|
|
986
|
+
.replace(/\s+/g, ' ')
|
|
987
|
+
.replace(/\s*\(\s*/g, '(')
|
|
988
|
+
.replace(/\s*\)\s*/g, ')')
|
|
989
|
+
.replace(/\s*,\s*/g, ',')
|
|
990
|
+
.trim();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private normalizeAggregateDefinition(definition: string): string {
|
|
994
|
+
let normalized = definition
|
|
995
|
+
.replace(/\s+/g, ' ')
|
|
996
|
+
.replace(/\s*\(\s*/g, '(')
|
|
997
|
+
.replace(/\s*\)\s*/g, ')')
|
|
998
|
+
.replace(/\s*,\s*/g, ',')
|
|
999
|
+
.trim();
|
|
1000
|
+
|
|
1001
|
+
const initCondMatch = normalized.match(/INITCOND\s*=\s*([^,)]+)/i);
|
|
1002
|
+
if (initCondMatch) {
|
|
1003
|
+
const initCondValue = initCondMatch[1].trim();
|
|
1004
|
+
const unquoted = initCondValue.replace(/^['"]|['"]$/g, '');
|
|
1005
|
+
if (/^\d+$/.test(unquoted)) {
|
|
1006
|
+
normalized = normalized.replace(/INITCOND\s*=\s*[^,)]+/i, `INITCOND = '${unquoted}'`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return normalized;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private extractFunctionBody(definition: string): string {
|
|
1014
|
+
const dollarQuoteMatch = definition.match(/AS\s+\$([^$]*)\$([\s\S]*?)\$\1\$/i);
|
|
1015
|
+
if (dollarQuoteMatch) {
|
|
1016
|
+
return dollarQuoteMatch[2].trim();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const bodyMatch = definition.match(/AS\s+\$\$([\s\S]*?)\$\$/i) || definition.match(/AS\s+['"]([\s\S]*?)['"]/i);
|
|
1020
|
+
if (bodyMatch) {
|
|
1021
|
+
return bodyMatch[1].trim();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return definition;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
private async getDatabaseFunctions(): Promise<DatabaseFunction[]> {
|
|
1028
|
+
const regularFunctions = await this.knex.raw(`
|
|
1029
|
+
SELECT
|
|
1030
|
+
p.proname as name,
|
|
1031
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
1032
|
+
pg_get_functiondef(p.oid) as definition,
|
|
1033
|
+
false as is_aggregate
|
|
1034
|
+
FROM pg_proc p
|
|
1035
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
1036
|
+
WHERE n.nspname = 'public'
|
|
1037
|
+
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
1038
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
1039
|
+
`);
|
|
1040
|
+
|
|
1041
|
+
const aggregateFunctions = await this.knex.raw(`
|
|
1042
|
+
SELECT
|
|
1043
|
+
p.proname as name,
|
|
1044
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
1045
|
+
a.aggtransfn::regproc::text as trans_func,
|
|
1046
|
+
a.aggfinalfn::regproc::text as final_func,
|
|
1047
|
+
a.agginitval as init_val,
|
|
1048
|
+
pg_catalog.format_type(a.aggtranstype, NULL) as state_type,
|
|
1049
|
+
true as is_aggregate
|
|
1050
|
+
FROM pg_proc p
|
|
1051
|
+
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
1052
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
1053
|
+
WHERE n.nspname = 'public'
|
|
1054
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
1055
|
+
`);
|
|
1056
|
+
|
|
1057
|
+
const result: DatabaseFunction[] = [];
|
|
1058
|
+
|
|
1059
|
+
for (const row of regularFunctions.rows || []) {
|
|
1060
|
+
const definition = row.definition || '';
|
|
1061
|
+
const name = row.name || '';
|
|
1062
|
+
const argumentsStr = row.arguments || '';
|
|
1063
|
+
|
|
1064
|
+
if (!definition) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const signature = `${name}(${argumentsStr})`;
|
|
1069
|
+
const body = this.normalizeFunctionBody(this.extractFunctionBody(definition));
|
|
1070
|
+
|
|
1071
|
+
result.push({
|
|
1072
|
+
name,
|
|
1073
|
+
signature,
|
|
1074
|
+
body,
|
|
1075
|
+
isAggregate: false,
|
|
1076
|
+
definition,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
for (const row of aggregateFunctions.rows || []) {
|
|
1081
|
+
const name = row.name || '';
|
|
1082
|
+
const argumentsStr = row.arguments || '';
|
|
1083
|
+
const transFunc = row.trans_func || '';
|
|
1084
|
+
const finalFunc = row.final_func || '';
|
|
1085
|
+
const initVal = row.init_val;
|
|
1086
|
+
const stateType = row.state_type || '';
|
|
1087
|
+
|
|
1088
|
+
const signature = `${name}(${argumentsStr})`;
|
|
1089
|
+
|
|
1090
|
+
let aggregateDef = `CREATE AGGREGATE ${name}(${argumentsStr}) (`;
|
|
1091
|
+
aggregateDef += `SFUNC = ${transFunc}, STYPE = ${stateType}`;
|
|
1092
|
+
|
|
1093
|
+
if (finalFunc) {
|
|
1094
|
+
aggregateDef += `, FINALFUNC = ${finalFunc}`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (initVal !== null && initVal !== undefined) {
|
|
1098
|
+
let initValStr: string;
|
|
1099
|
+
if (typeof initVal === 'string') {
|
|
1100
|
+
initValStr = `'${initVal}'`;
|
|
1101
|
+
} else {
|
|
1102
|
+
const numStr = String(initVal);
|
|
1103
|
+
initValStr = /^\d+$/.test(numStr) ? `'${numStr}'` : numStr;
|
|
1104
|
+
}
|
|
1105
|
+
aggregateDef += `, INITCOND = ${initValStr}`;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
aggregateDef += ');';
|
|
1109
|
+
|
|
1110
|
+
result.push({
|
|
1111
|
+
name,
|
|
1112
|
+
signature,
|
|
1113
|
+
body: this.normalizeAggregateDefinition(aggregateDef),
|
|
1114
|
+
isAggregate: true,
|
|
1115
|
+
definition: aggregateDef,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private async handleFunctions(up: Callbacks, down: Callbacks) {
|
|
1123
|
+
if (!this.functionsFilePath) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const definedFunctions = parseFunctionsFile(this.functionsFilePath);
|
|
1128
|
+
|
|
1129
|
+
if (definedFunctions.length === 0) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const dbFunctions = await this.getDatabaseFunctions();
|
|
1134
|
+
const dbFunctionsBySignature = new Map<string, DatabaseFunction>();
|
|
1135
|
+
for (const func of dbFunctions) {
|
|
1136
|
+
dbFunctionsBySignature.set(func.signature, func);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const definedFunctionsBySignature = new Map<string, ParsedFunction>();
|
|
1140
|
+
for (const func of definedFunctions) {
|
|
1141
|
+
definedFunctionsBySignature.set(func.signature, func);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const functionsToRestore: { func: DatabaseFunction; definition: string }[] = [];
|
|
1145
|
+
|
|
1146
|
+
for (const definedFunc of definedFunctions) {
|
|
1147
|
+
const dbFunc = dbFunctionsBySignature.get(definedFunc.signature);
|
|
1148
|
+
|
|
1149
|
+
if (!dbFunc) {
|
|
1150
|
+
up.push(() => {
|
|
1151
|
+
this.writer.writeLine(`await knex.raw(\`${definedFunc.fullDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
down.push(() => {
|
|
1155
|
+
const isAggregate = definedFunc.isAggregate;
|
|
1156
|
+
const dropMatch = definedFunc.fullDefinition.match(/CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)\s+([^(]+)\(/i);
|
|
1157
|
+
if (dropMatch) {
|
|
1158
|
+
const functionName = dropMatch[3].trim();
|
|
1159
|
+
const argsMatch = definedFunc.fullDefinition.match(
|
|
1160
|
+
/CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)\s+[^(]+\(([^)]*)\)/i,
|
|
1161
|
+
);
|
|
1162
|
+
const args = argsMatch ? argsMatch[3].trim() : '';
|
|
1163
|
+
const dropType = isAggregate ? 'AGGREGATE' : 'FUNCTION';
|
|
1164
|
+
this.writer
|
|
1165
|
+
.writeLine(`await knex.raw(\`DROP ${dropType} IF EXISTS ${functionName}${args ? `(${args})` : ''}\`);`)
|
|
1166
|
+
.blankLine();
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
} else {
|
|
1170
|
+
const dbBody = dbFunc.isAggregate
|
|
1171
|
+
? this.normalizeAggregateDefinition(dbFunc.body)
|
|
1172
|
+
: this.normalizeFunctionBody(dbFunc.body);
|
|
1173
|
+
const definedBody = definedFunc.isAggregate
|
|
1174
|
+
? this.normalizeAggregateDefinition(definedFunc.body)
|
|
1175
|
+
: this.normalizeFunctionBody(definedFunc.body);
|
|
1176
|
+
|
|
1177
|
+
if (dbBody !== definedBody) {
|
|
1178
|
+
const oldDefinition = dbFunc.definition || dbFunc.body;
|
|
1179
|
+
|
|
1180
|
+
up.push(() => {
|
|
1181
|
+
this.writer.writeLine(`await knex.raw(\`${definedFunc.fullDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
down.push(() => {
|
|
1185
|
+
if (oldDefinition) {
|
|
1186
|
+
this.writer.writeLine(`await knex.raw(\`${oldDefinition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
for (const dbFunc of dbFunctions) {
|
|
1194
|
+
if (!definedFunctionsBySignature.has(dbFunc.signature)) {
|
|
1195
|
+
const definition = dbFunc.definition || dbFunc.body;
|
|
1196
|
+
|
|
1197
|
+
if (definition) {
|
|
1198
|
+
functionsToRestore.push({ func: dbFunc, definition });
|
|
1199
|
+
|
|
1200
|
+
down.push(() => {
|
|
1201
|
+
const argsMatch = dbFunc.signature.match(/\(([^)]*)\)/);
|
|
1202
|
+
const args = argsMatch ? argsMatch[1] : '';
|
|
1203
|
+
const dropType = dbFunc.isAggregate ? 'AGGREGATE' : 'FUNCTION';
|
|
1204
|
+
this.writer
|
|
1205
|
+
.writeLine(`await knex.raw(\`DROP ${dropType} IF EXISTS ${dbFunc.name}${args ? `(${args})` : ''}\`);`)
|
|
1206
|
+
.blankLine();
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
for (const { definition } of functionsToRestore) {
|
|
1213
|
+
up.push(() => {
|
|
1214
|
+
this.writer.writeLine(`await knex.raw(\`${definition.replace(/`/g, '\\`')}\`);`).blankLine();
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
921
1218
|
}
|
|
922
1219
|
|
|
923
1220
|
export const getMigrationDate = () => {
|