@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/dist/bin/gqm.cjs +656 -59
  3. package/dist/cjs/index.cjs +2700 -2133
  4. package/dist/esm/migrations/generate-functions.d.ts +2 -0
  5. package/dist/esm/migrations/generate-functions.js +60 -0
  6. package/dist/esm/migrations/generate-functions.js.map +1 -0
  7. package/dist/esm/migrations/generate.d.ts +9 -1
  8. package/dist/esm/migrations/generate.js +269 -33
  9. package/dist/esm/migrations/generate.js.map +1 -1
  10. package/dist/esm/migrations/index.d.ts +2 -0
  11. package/dist/esm/migrations/index.js +2 -0
  12. package/dist/esm/migrations/index.js.map +1 -1
  13. package/dist/esm/migrations/types.d.ts +7 -0
  14. package/dist/esm/migrations/types.js +2 -0
  15. package/dist/esm/migrations/types.js.map +1 -0
  16. package/dist/esm/migrations/update-functions.d.ts +3 -0
  17. package/dist/esm/migrations/update-functions.js +177 -0
  18. package/dist/esm/migrations/update-functions.js.map +1 -0
  19. package/dist/esm/models/model-definitions.d.ts +4 -1
  20. package/dist/esm/resolvers/filters.js +76 -14
  21. package/dist/esm/resolvers/filters.js.map +1 -1
  22. package/dist/esm/resolvers/selects.js +20 -2
  23. package/dist/esm/resolvers/selects.js.map +1 -1
  24. package/dist/esm/resolvers/utils.d.ts +1 -0
  25. package/dist/esm/resolvers/utils.js +29 -0
  26. package/dist/esm/resolvers/utils.js.map +1 -1
  27. package/docs/docs/3-fields.md +149 -0
  28. package/docs/docs/5-migrations.md +9 -1
  29. package/package.json +2 -2
  30. package/src/bin/gqm/gqm.ts +44 -5
  31. package/src/bin/gqm/parse-functions.ts +141 -0
  32. package/src/bin/gqm/settings.ts +7 -0
  33. package/src/bin/gqm/utils.ts +1 -0
  34. package/src/migrations/generate-functions.ts +74 -0
  35. package/src/migrations/generate.ts +334 -41
  36. package/src/migrations/index.ts +2 -0
  37. package/src/migrations/types.ts +7 -0
  38. package/src/migrations/update-functions.ts +221 -0
  39. package/src/models/model-definitions.ts +4 -1
  40. package/src/resolvers/filters.ts +88 -25
  41. package/src/resolvers/selects.ts +22 -5
  42. package/src/resolvers/utils.ts +44 -0
@@ -0,0 +1,2 @@
1
+ import { Knex } from 'knex';
2
+ export declare const generateFunctionsFromDatabase: (knex: Knex) => Promise<string>;
@@ -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
- constructor(knex: Knex, models: Models);
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
- constructor(knex, models) {
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.filter(not(isInherited))) {
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
- this.renameFields(model, model.fields.filter(not(isInherited)).filter(({ oldName }) => oldName), up, down);
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.filter(isUpdatableField)) {
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(model, fields, up, down) {
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(model.name, () => {
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(model.name, () => {
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[model.name], field.kind === 'relation' ? `${field.oldName}Id` : field.oldName).name =
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.filter(and(isUpdatableField, not(isInherited)))) {
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.writer
417
- .write(`await knex('${model.name}Revision').update(`)
418
- .inlineBlock(() => {
419
- for (const { name, kind: type } of missingRevisionFields) {
420
- const col = type === 'relation' ? `${name}Id` : name;
421
- this.writer
422
- .write(`${col}: knex.raw('(select "${col}" from "${model.name}" where "${model.name}".id = "${model.name}Revision"."${typeToField(model.name)}Id")'),`)
423
- .newLine();
424
- }
425
- })
426
- .write(');')
427
- .newLine()
428
- .blankLine();
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();