@smartive/graphql-magic 23.4.1 → 23.5.0-next.2
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 +2 -2
- package/dist/bin/gqm.cjs +568 -59
- package/dist/cjs/index.cjs +2635 -2148
- 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 +5 -1
- package/dist/esm/migrations/generate.js +151 -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 +14 -0
- package/dist/esm/migrations/update-functions.js +201 -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 +192 -41
- package/src/migrations/index.ts +2 -0
- package/src/migrations/types.ts +7 -0
- package/src/migrations/update-functions.ts +247 -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
package/docs/docs/3-fields.md
CHANGED
|
@@ -341,3 +341,152 @@ the reverse relation will automatically be `Post.comments`. With `reverse` this
|
|
|
341
341
|
### `onDelete`
|
|
342
342
|
|
|
343
343
|
Only available on relation fields. Can be `"cascade"` (default), `"restrict"` or `"set-null"`.
|
|
344
|
+
|
|
345
|
+
### `generateAs`
|
|
346
|
+
|
|
347
|
+
Only available on entity fields. Allows you to define fields that are computed from SQL expressions rather than stored directly. This option accepts an object with:
|
|
348
|
+
|
|
349
|
+
- `expression`: A SQL expression string that defines how the field is computed
|
|
350
|
+
- `type`: One of `'virtual'`, `'stored'`, or `'expression'`
|
|
351
|
+
|
|
352
|
+
#### `type: 'virtual'` and `type: 'stored'`
|
|
353
|
+
|
|
354
|
+
These types create SQL generated columns in the database. The difference is:
|
|
355
|
+
|
|
356
|
+
- **`'virtual'`**: The value is computed on-the-fly when queried (not stored in the database)
|
|
357
|
+
- **`'stored'`**: The value is computed and stored in the database (takes up space but faster queries)
|
|
358
|
+
|
|
359
|
+
Both types affect the SQL schema and create actual database columns.
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
{
|
|
365
|
+
name: 'Product',
|
|
366
|
+
fields: [
|
|
367
|
+
{
|
|
368
|
+
name: 'price',
|
|
369
|
+
type: 'Float',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'quantity',
|
|
373
|
+
type: 'Int',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'totalPrice',
|
|
377
|
+
type: 'Float',
|
|
378
|
+
generateAs: {
|
|
379
|
+
expression: 'price * quantity',
|
|
380
|
+
type: 'stored'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
This creates a `totalPrice` column in the database that is automatically computed as `price * quantity`. The expression can reference other columns in the same table.
|
|
388
|
+
|
|
389
|
+
#### `type: 'expression'`
|
|
390
|
+
|
|
391
|
+
This type creates a field that is **not** stored in the database schema. Instead, the expression is evaluated at query time during SELECT operations. This is useful for:
|
|
392
|
+
|
|
393
|
+
- Computed fields that don't need to be stored
|
|
394
|
+
- Fields that reference columns but shouldn't create database columns
|
|
395
|
+
- Dynamic calculations that should always use the latest data
|
|
396
|
+
|
|
397
|
+
Example:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
{
|
|
401
|
+
name: 'Product',
|
|
402
|
+
fields: [
|
|
403
|
+
{
|
|
404
|
+
name: 'price',
|
|
405
|
+
type: 'Float',
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: 'quantity',
|
|
409
|
+
type: 'Int',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'totalPrice',
|
|
413
|
+
type: 'Float',
|
|
414
|
+
generateAs: {
|
|
415
|
+
expression: 'price * quantity',
|
|
416
|
+
type: 'expression'
|
|
417
|
+
},
|
|
418
|
+
filterable: true,
|
|
419
|
+
comparable: true,
|
|
420
|
+
orderable: true
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
This creates a `totalPrice` field that is computed at query time. The expression can reference other columns in the table, and column names in the expression are automatically resolved with proper table aliases.
|
|
427
|
+
|
|
428
|
+
**Important notes for `type: 'expression'`:**
|
|
429
|
+
|
|
430
|
+
- The field does **not** create a database column
|
|
431
|
+
- The expression is evaluated during SELECT queries
|
|
432
|
+
- Column references in the expression are automatically prefixed with the table alias
|
|
433
|
+
- Can be combined with `filterable`, `comparable`, `orderable`, and `searchable` options
|
|
434
|
+
- Works with all filter types (equality, arrays, null checks, comparisons)
|
|
435
|
+
|
|
436
|
+
**Expression examples:**
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// Simple calculation
|
|
440
|
+
{
|
|
441
|
+
name: 'totalPrice',
|
|
442
|
+
type: 'Float',
|
|
443
|
+
generateAs: {
|
|
444
|
+
expression: 'price * quantity',
|
|
445
|
+
type: 'expression'
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Using SQL functions
|
|
450
|
+
{
|
|
451
|
+
name: 'fullName',
|
|
452
|
+
type: 'String',
|
|
453
|
+
generateAs: {
|
|
454
|
+
expression: "COALESCE(firstName || ' ' || lastName, 'Unknown')",
|
|
455
|
+
type: 'expression'
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Conditional logic
|
|
460
|
+
{
|
|
461
|
+
name: 'discountedPrice',
|
|
462
|
+
type: 'Float',
|
|
463
|
+
generateAs: {
|
|
464
|
+
expression: 'CASE WHEN discount > 0 THEN price * (1 - discount) ELSE price END',
|
|
465
|
+
type: 'expression'
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Filtering expression fields:**
|
|
471
|
+
|
|
472
|
+
When `filterable: true` is set on an expression field, you can filter by it:
|
|
473
|
+
|
|
474
|
+
```graphql
|
|
475
|
+
query {
|
|
476
|
+
products(where: { totalPrice_GT: 100 }) {
|
|
477
|
+
totalPrice
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Ordering expression fields:**
|
|
483
|
+
|
|
484
|
+
When `orderable: true` is set on an expression field, you can order by it:
|
|
485
|
+
|
|
486
|
+
```graphql
|
|
487
|
+
query {
|
|
488
|
+
products(orderBy: [{ totalPrice: DESC }]) {
|
|
489
|
+
totalPrice
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
@@ -20,9 +20,17 @@ Note: if you are in a `feat/<feature-name>` branch, the script will use that as
|
|
|
20
20
|
|
|
21
21
|
This will generate a migration file in the `migrations` folder (without running the migration itself yet). Check whether it needs to be adapted.
|
|
22
22
|
|
|
23
|
+
### Generated columns
|
|
24
|
+
|
|
25
|
+
Fields with `generateAs` are handled specially in migrations:
|
|
26
|
+
|
|
27
|
+
- **`type: 'virtual'` or `type: 'stored'`**: These create SQL `GENERATED ALWAYS AS` columns in the database. The migration generator will create these columns automatically.
|
|
28
|
+
|
|
29
|
+
- **`type: 'expression'`**: These fields do **not** create database columns. They are computed at query time and will not appear in migration files. This means you can add or remove expression fields without needing a migration.
|
|
30
|
+
|
|
23
31
|
## Running migrations
|
|
24
32
|
|
|
25
|
-
Migrations themselves are managed with `knex` (see the [knex migration docs](https://knexjs.org/guide/migrations.html)).
|
|
33
|
+
Migrations themselves are managed with `knex` (see the [knex migration docs](https://knexjs.org/guide/migrations.html)).
|
|
26
34
|
|
|
27
35
|
For example, to migrate to the latest version (using `env-cmd` to add db connection variables in `.env`):
|
|
28
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartive/graphql-magic",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.5.0-next.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"knex": "^3.0.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@smartive/eslint-config": "7.0.
|
|
71
|
+
"@smartive/eslint-config": "7.0.1",
|
|
72
72
|
"@smartive/prettier-config": "3.1.2",
|
|
73
73
|
"@types/jest": "30.0.0",
|
|
74
74
|
"@types/lodash": "4.17.23",
|
package/src/bin/gqm/gqm.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
getMigrationDate,
|
|
13
13
|
printSchemaFromModels,
|
|
14
14
|
} from '../..';
|
|
15
|
+
import { generateFunctionsFromDatabase } from '../../migrations/generate-functions';
|
|
16
|
+
import { updateFunctions } from '../../migrations/update-functions';
|
|
15
17
|
import { DateLibrary } from '../../utils/dates';
|
|
16
18
|
import { generateGraphqlApiTypes, generateGraphqlClientTypes } from './codegen';
|
|
19
|
+
import { parseFunctionsFile } from './parse-functions';
|
|
17
20
|
import { parseKnexfile } from './parse-knexfile';
|
|
18
21
|
import { parseModels } from './parse-models';
|
|
19
22
|
import { generatePermissionTypes } from './permissions';
|
|
@@ -76,7 +79,9 @@ program
|
|
|
76
79
|
|
|
77
80
|
try {
|
|
78
81
|
const models = await parseModels();
|
|
79
|
-
const
|
|
82
|
+
const functionsPath = await getSetting('functionsPath');
|
|
83
|
+
const parsedFunctions = parseFunctionsFile(functionsPath);
|
|
84
|
+
const migrations = await new MigrationGenerator(db, models, parsedFunctions).generate();
|
|
80
85
|
|
|
81
86
|
writeToFile(`migrations/${date || getMigrationDate()}_${name}.ts`, migrations);
|
|
82
87
|
} finally {
|
|
@@ -93,7 +98,9 @@ program
|
|
|
93
98
|
|
|
94
99
|
try {
|
|
95
100
|
const models = await parseModels();
|
|
96
|
-
const
|
|
101
|
+
const functionsPath = await getSetting('functionsPath');
|
|
102
|
+
const parsedFunctions = parseFunctionsFile(functionsPath);
|
|
103
|
+
const mg = new MigrationGenerator(db, models, parsedFunctions);
|
|
97
104
|
await mg.generate();
|
|
98
105
|
|
|
99
106
|
if (mg.needsMigration) {
|
|
@@ -106,10 +113,42 @@ program
|
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
program
|
|
109
|
-
.command('
|
|
116
|
+
.command('generate-functions')
|
|
117
|
+
.description('Generate functions.ts file from database')
|
|
118
|
+
.action(async () => {
|
|
119
|
+
const knexfile = await parseKnexfile();
|
|
120
|
+
const db = knex(knexfile);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const functionsPath = await getSetting('functionsPath');
|
|
124
|
+
const functions = await generateFunctionsFromDatabase(db);
|
|
125
|
+
writeToFile(functionsPath, functions);
|
|
126
|
+
} finally {
|
|
127
|
+
await db.destroy();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
program
|
|
132
|
+
.command('update-functions')
|
|
133
|
+
.description('Update database functions from functions.ts file')
|
|
134
|
+
.action(async () => {
|
|
135
|
+
const knexfile = await parseKnexfile();
|
|
136
|
+
const db = knex(knexfile);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const functionsPath = await getSetting('functionsPath');
|
|
140
|
+
const parsedFunctions = parseFunctionsFile(functionsPath);
|
|
141
|
+
await updateFunctions(db, parsedFunctions);
|
|
142
|
+
} finally {
|
|
143
|
+
await db.destroy();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command('*')
|
|
110
149
|
.description('Invalid command')
|
|
111
|
-
.action(() => {
|
|
112
|
-
console.error(
|
|
150
|
+
.action((command) => {
|
|
151
|
+
console.error(`Invalid command: ${command}\nSee --help for a list of available commands.`);
|
|
113
152
|
process.exit(1);
|
|
114
153
|
});
|
|
115
154
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { IndentationText, Project } from 'ts-morph';
|
|
3
|
+
import { ParsedFunction } from '../../migrations/types';
|
|
4
|
+
import { staticEval } from './static-eval';
|
|
5
|
+
import { findDeclarationInFile } from './utils';
|
|
6
|
+
|
|
7
|
+
const normalizeWhitespace = (str: string): string => {
|
|
8
|
+
return str
|
|
9
|
+
.replace(/\s+/g, ' ')
|
|
10
|
+
.replace(/\s*\(\s*/g, '(')
|
|
11
|
+
.replace(/\s*\)\s*/g, ')')
|
|
12
|
+
.replace(/\s*,\s*/g, ',')
|
|
13
|
+
.replace(/\s*;\s*/g, ';')
|
|
14
|
+
.trim();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const normalizeFunctionBody = (body: string): string => {
|
|
18
|
+
return normalizeWhitespace(body);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const normalizeAggregateDefinition = (definition: string): string => {
|
|
22
|
+
let normalized = normalizeWhitespace(definition);
|
|
23
|
+
|
|
24
|
+
const initCondMatch = normalized.match(/INITCOND\s*=\s*([^,)]+)/i);
|
|
25
|
+
if (initCondMatch) {
|
|
26
|
+
const initCondValue = initCondMatch[1].trim();
|
|
27
|
+
const unquoted = initCondValue.replace(/^['"]|['"]$/g, '');
|
|
28
|
+
if (/^\d+$/.test(unquoted)) {
|
|
29
|
+
normalized = normalized.replace(/INITCOND\s*=\s*[^,)]+/i, `INITCOND = '${unquoted}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalized;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const extractFunctionSignature = (definition: string, isAggregate: boolean): string | null => {
|
|
37
|
+
if (isAggregate) {
|
|
38
|
+
const createMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+([^(]+)\(/i);
|
|
39
|
+
if (!createMatch) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const functionNamePart = createMatch[2].trim().replace(/^[^.]+\./, '');
|
|
44
|
+
const argsMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+[^(]+\(([^)]*)\)/i);
|
|
45
|
+
const args = argsMatch ? argsMatch[2].trim() : '';
|
|
46
|
+
|
|
47
|
+
return `${functionNamePart}(${args})`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const createMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+([^(]+)\(/i);
|
|
51
|
+
if (!createMatch) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const functionNamePart = createMatch[2].trim().replace(/^[^.]+\./, '');
|
|
56
|
+
const fullArgsMatch = definition.match(
|
|
57
|
+
/CREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+[^(]+\(([\s\S]*?)\)\s*(RETURNS|LANGUAGE|AS|STRICT|IMMUTABLE|STABLE|VOLATILE|SECURITY)/i,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!fullArgsMatch) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const argsSection = fullArgsMatch[2].trim();
|
|
65
|
+
const args = argsSection
|
|
66
|
+
.split(/\s*,\s*/)
|
|
67
|
+
.map((arg) => {
|
|
68
|
+
return arg.trim().replace(/\s+/g, ' ');
|
|
69
|
+
})
|
|
70
|
+
.join(', ');
|
|
71
|
+
|
|
72
|
+
return `${functionNamePart}(${args})`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const extractFunctionBody = (definition: string): string => {
|
|
76
|
+
const dollarQuoteMatch = definition.match(/AS\s+\$([^$]*)\$([\s\S]*?)\$\1\$/i);
|
|
77
|
+
if (dollarQuoteMatch) {
|
|
78
|
+
return dollarQuoteMatch[2].trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const bodyMatch = definition.match(/AS\s+\$\$([\s\S]*?)\$\$/i) || definition.match(/AS\s+['"]([\s\S]*?)['"]/i);
|
|
82
|
+
if (bodyMatch) {
|
|
83
|
+
return bodyMatch[1].trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return definition;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const parseFunctionsFile = (filePath: string): ParsedFunction[] => {
|
|
90
|
+
if (!existsSync(filePath)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const project = new Project({
|
|
95
|
+
manipulationSettings: {
|
|
96
|
+
indentationText: IndentationText.TwoSpaces,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const functionsDeclaration = findDeclarationInFile(sourceFile, 'functions');
|
|
103
|
+
const functionsArray = staticEval(functionsDeclaration, {}) as string[];
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(functionsArray)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsedFunctions: ParsedFunction[] = [];
|
|
110
|
+
|
|
111
|
+
for (const definition of functionsArray) {
|
|
112
|
+
if (!definition || typeof definition !== 'string') {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const trimmedDefinition = definition.trim();
|
|
117
|
+
const isAggregate = /CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE/i.test(trimmedDefinition);
|
|
118
|
+
const signature = extractFunctionSignature(trimmedDefinition, isAggregate);
|
|
119
|
+
|
|
120
|
+
if (!signature) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nameMatch = signature.match(/^([^(]+)\(/);
|
|
125
|
+
const name = nameMatch ? nameMatch[1].trim().split('.').pop() || '' : '';
|
|
126
|
+
const body = isAggregate ? trimmedDefinition : extractFunctionBody(trimmedDefinition);
|
|
127
|
+
|
|
128
|
+
parsedFunctions.push({
|
|
129
|
+
name,
|
|
130
|
+
signature,
|
|
131
|
+
body: isAggregate ? normalizeAggregateDefinition(body) : normalizeFunctionBody(body),
|
|
132
|
+
fullDefinition: trimmedDefinition,
|
|
133
|
+
isAggregate,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return parsedFunctions;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
};
|
package/src/bin/gqm/settings.ts
CHANGED
|
@@ -39,6 +39,13 @@ const DEFAULTS = {
|
|
|
39
39
|
ensureFileExists(path, EMPTY_MODELS);
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
|
+
functionsPath: {
|
|
43
|
+
question: 'What is the PostgreSQL functions file path?',
|
|
44
|
+
defaultValue: 'src/config/functions.ts',
|
|
45
|
+
init: (path: string) => {
|
|
46
|
+
ensureFileExists(path, `export const functions: string[] = [];\n`);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
42
49
|
generatedFolderPath: {
|
|
43
50
|
question: 'What is the path for generated stuff?',
|
|
44
51
|
defaultValue: 'src/generated',
|
package/src/bin/gqm/utils.ts
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
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 `export const functions: string[] = [];\n`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const functionsArrayString = functions.map((func) => ` ${JSON.stringify(func)}`).join(',\n');
|
|
72
|
+
|
|
73
|
+
return `export const functions: string[] = [\n${functionsArrayString},\n];\n`;
|
|
74
|
+
};
|