@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.
Files changed (41) hide show
  1. package/.github/workflows/release.yml +8 -2
  2. package/CHANGELOG.md +4 -2
  3. package/dist/bin/gqm.cjs +642 -63
  4. package/dist/cjs/index.cjs +2767 -2107
  5. package/dist/esm/migrations/generate-functions.d.ts +2 -0
  6. package/dist/esm/migrations/generate-functions.js +59 -0
  7. package/dist/esm/migrations/generate-functions.js.map +1 -0
  8. package/dist/esm/migrations/generate.d.ts +8 -1
  9. package/dist/esm/migrations/generate.js +273 -33
  10. package/dist/esm/migrations/generate.js.map +1 -1
  11. package/dist/esm/migrations/index.d.ts +2 -0
  12. package/dist/esm/migrations/index.js +2 -0
  13. package/dist/esm/migrations/index.js.map +1 -1
  14. package/dist/esm/migrations/parse-functions.d.ts +8 -0
  15. package/dist/esm/migrations/parse-functions.js +105 -0
  16. package/dist/esm/migrations/parse-functions.js.map +1 -0
  17. package/dist/esm/migrations/update-functions.d.ts +2 -0
  18. package/dist/esm/migrations/update-functions.js +174 -0
  19. package/dist/esm/migrations/update-functions.js.map +1 -0
  20. package/dist/esm/models/model-definitions.d.ts +4 -1
  21. package/dist/esm/resolvers/filters.js +73 -14
  22. package/dist/esm/resolvers/filters.js.map +1 -1
  23. package/dist/esm/resolvers/selects.js +33 -2
  24. package/dist/esm/resolvers/selects.js.map +1 -1
  25. package/dist/esm/resolvers/utils.d.ts +1 -0
  26. package/dist/esm/resolvers/utils.js +22 -0
  27. package/dist/esm/resolvers/utils.js.map +1 -1
  28. package/docs/docs/3-fields.md +149 -0
  29. package/docs/docs/5-migrations.md +9 -1
  30. package/package.json +5 -1
  31. package/src/bin/gqm/gqm.ts +40 -5
  32. package/src/bin/gqm/settings.ts +7 -0
  33. package/src/migrations/generate-functions.ts +72 -0
  34. package/src/migrations/generate.ts +338 -41
  35. package/src/migrations/index.ts +2 -0
  36. package/src/migrations/parse-functions.ts +140 -0
  37. package/src/migrations/update-functions.ts +216 -0
  38. package/src/models/model-definitions.ts +4 -1
  39. package/src/resolvers/filters.ts +81 -25
  40. package/src/resolvers/selects.ts +38 -5
  41. 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.filter(not(isInherited))) {
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
- this.renameFields(
142
- model,
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((field) => !field.generateAs && this.hasChanged(model, field));
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.filter(isUpdatableField)) {
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(model: EntityModel, fields: EntityField[], up: Callbacks, down: Callbacks) {
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(model.name, () => {
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(model.name, () => {
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[model.name], field.kind === 'relation' ? `${field.oldName!}Id` : field.oldName!).name =
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.filter(and(isUpdatableField, not(isInherited)))) {
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
- this.writer
550
- .write(`await knex('${model.name}Revision').update(`)
551
- .inlineBlock(() => {
552
- for (const { name, kind: type } of missingRevisionFields) {
553
- const col = type === 'relation' ? `${name}Id` : name;
554
- this.writer
555
- .write(
556
- `${col}: knex.raw('(select "${col}" from "${model.name}" where "${model.name}".id = "${
557
- model.name
558
- }Revision"."${typeToField(model.name)}Id")'),`,
559
- )
560
- .newLine();
561
- }
562
- })
563
- .write(');')
564
- .newLine()
565
- .blankLine();
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 = () => {
@@ -1,3 +1,5 @@
1
1
  // created from 'create-ts-index'
2
2
 
3
3
  export * from './generate';
4
+ export * from './generate-functions';
5
+ export * from './update-functions';