@smartive/graphql-magic 23.4.0-next.8 → 23.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/dist/bin/gqm.cjs +125 -656
- package/dist/cjs/index.cjs +2132 -2702
- package/dist/esm/migrations/generate.d.ts +1 -9
- package/dist/esm/migrations/generate.js +33 -269
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/index.d.ts +0 -2
- package/dist/esm/migrations/index.js +0 -2
- package/dist/esm/migrations/index.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +1 -4
- package/dist/esm/resolvers/filters.js +14 -73
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/selects.js +2 -33
- package/dist/esm/resolvers/selects.js.map +1 -1
- package/dist/esm/resolvers/utils.d.ts +0 -1
- package/dist/esm/resolvers/utils.js +0 -22
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/docs/docs/3-fields.md +0 -149
- package/docs/docs/5-migrations.md +1 -9
- package/package.json +2 -2
- package/src/bin/gqm/gqm.ts +5 -44
- package/src/bin/gqm/settings.ts +0 -7
- package/src/bin/gqm/static-eval.ts +102 -0
- package/src/bin/gqm/utils.ts +0 -1
- package/src/migrations/generate.ts +41 -334
- package/src/migrations/index.ts +0 -2
- package/src/models/model-definitions.ts +1 -4
- package/src/resolvers/filters.ts +25 -81
- package/src/resolvers/selects.ts +5 -38
- package/src/resolvers/utils.ts +0 -32
- package/dist/esm/migrations/generate-functions.d.ts +0 -2
- package/dist/esm/migrations/generate-functions.js +0 -60
- package/dist/esm/migrations/generate-functions.js.map +0 -1
- package/dist/esm/migrations/types.d.ts +0 -7
- package/dist/esm/migrations/types.js +0 -2
- package/dist/esm/migrations/types.js.map +0 -1
- package/dist/esm/migrations/update-functions.d.ts +0 -3
- package/dist/esm/migrations/update-functions.js +0 -177
- package/dist/esm/migrations/update-functions.js.map +0 -1
- package/src/bin/gqm/parse-functions.ts +0 -141
- package/src/migrations/generate-functions.ts +0 -74
- package/src/migrations/types.ts +0 -7
- package/src/migrations/update-functions.ts +0 -221
package/docs/docs/3-fields.md
CHANGED
|
@@ -341,152 +341,3 @@ 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,17 +20,9 @@ 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
|
-
|
|
31
23
|
## Running migrations
|
|
32
24
|
|
|
33
|
-
Migrations themselves are managed with `knex` (see the [knex migration docs](https://knexjs.org/guide/migrations.html)).
|
|
25
|
+
Migrations themselves are managed with `knex` (see the [knex migration docs](https://knexjs.org/guide/migrations.html)).
|
|
34
26
|
|
|
35
27
|
For example, to migrate to the latest version (using `env-cmd` to add db connection variables in `.env`):
|
|
36
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartive/graphql-magic",
|
|
3
|
-
"version": "23.4.
|
|
3
|
+
"version": "23.4.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"conventional-changelog-conventionalcommits": "9.1.0",
|
|
78
78
|
"create-ts-index": "1.14.0",
|
|
79
79
|
"del-cli": "7.0.0",
|
|
80
|
-
"esbuild": "0.27.
|
|
80
|
+
"esbuild": "0.27.3",
|
|
81
81
|
"eslint": "9.39.2",
|
|
82
82
|
"jest": "30.2.0",
|
|
83
83
|
"mock-knex": "0.4.13",
|
package/src/bin/gqm/gqm.ts
CHANGED
|
@@ -12,11 +12,8 @@ import {
|
|
|
12
12
|
getMigrationDate,
|
|
13
13
|
printSchemaFromModels,
|
|
14
14
|
} from '../..';
|
|
15
|
-
import { generateFunctionsFromDatabase } from '../../migrations/generate-functions';
|
|
16
|
-
import { updateFunctions } from '../../migrations/update-functions';
|
|
17
15
|
import { DateLibrary } from '../../utils/dates';
|
|
18
16
|
import { generateGraphqlApiTypes, generateGraphqlClientTypes } from './codegen';
|
|
19
|
-
import { parseFunctionsFile } from './parse-functions';
|
|
20
17
|
import { parseKnexfile } from './parse-knexfile';
|
|
21
18
|
import { parseModels } from './parse-models';
|
|
22
19
|
import { generatePermissionTypes } from './permissions';
|
|
@@ -79,9 +76,7 @@ program
|
|
|
79
76
|
|
|
80
77
|
try {
|
|
81
78
|
const models = await parseModels();
|
|
82
|
-
const
|
|
83
|
-
const parsedFunctions = parseFunctionsFile(functionsPath);
|
|
84
|
-
const migrations = await new MigrationGenerator(db, models, parsedFunctions).generate();
|
|
79
|
+
const migrations = await new MigrationGenerator(db, models).generate();
|
|
85
80
|
|
|
86
81
|
writeToFile(`migrations/${date || getMigrationDate()}_${name}.ts`, migrations);
|
|
87
82
|
} finally {
|
|
@@ -98,9 +93,7 @@ program
|
|
|
98
93
|
|
|
99
94
|
try {
|
|
100
95
|
const models = await parseModels();
|
|
101
|
-
const
|
|
102
|
-
const parsedFunctions = parseFunctionsFile(functionsPath);
|
|
103
|
-
const mg = new MigrationGenerator(db, models, parsedFunctions);
|
|
96
|
+
const mg = new MigrationGenerator(db, models);
|
|
104
97
|
await mg.generate();
|
|
105
98
|
|
|
106
99
|
if (mg.needsMigration) {
|
|
@@ -113,42 +106,10 @@ program
|
|
|
113
106
|
});
|
|
114
107
|
|
|
115
108
|
program
|
|
116
|
-
.command('
|
|
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('*')
|
|
109
|
+
.command('*', { noHelp: true })
|
|
149
110
|
.description('Invalid command')
|
|
150
|
-
.action((
|
|
151
|
-
console.error(
|
|
111
|
+
.action(() => {
|
|
112
|
+
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
|
|
152
113
|
process.exit(1);
|
|
153
114
|
});
|
|
154
115
|
|
package/src/bin/gqm/settings.ts
CHANGED
|
@@ -39,13 +39,6 @@ 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
|
-
},
|
|
49
42
|
generatedFolderPath: {
|
|
50
43
|
question: 'What is the path for generated stuff?',
|
|
51
44
|
defaultValue: 'src/generated',
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
CaseClause,
|
|
5
5
|
ElementAccessExpression,
|
|
6
6
|
Identifier,
|
|
7
|
+
ImportSpecifier,
|
|
7
8
|
Node,
|
|
8
9
|
ObjectLiteralExpression,
|
|
9
10
|
PrefixUnaryExpression,
|
|
10
11
|
ShorthandPropertyAssignment,
|
|
12
|
+
StringLiteral,
|
|
11
13
|
SyntaxKind,
|
|
12
14
|
TemplateExpression,
|
|
13
15
|
TemplateTail,
|
|
@@ -77,6 +79,106 @@ const VISITOR: Visitor<unknown, Dictionary<unknown>> = {
|
|
|
77
79
|
}),
|
|
78
80
|
[SyntaxKind.SpreadElement]: (node, context) => staticEval(node.getExpression(), context),
|
|
79
81
|
[SyntaxKind.SpreadAssignment]: (node, context) => staticEval(node.getExpression(), context),
|
|
82
|
+
[SyntaxKind.ImportSpecifier]: (node: ImportSpecifier, context) => {
|
|
83
|
+
const nameNode = node.getNameNode();
|
|
84
|
+
const name = nameNode.getText();
|
|
85
|
+
|
|
86
|
+
if (name in KNOWN_IDENTIFIERS) {
|
|
87
|
+
return KNOWN_IDENTIFIERS[name];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (nameNode instanceof StringLiteral) {
|
|
91
|
+
throw new Error(`Cannot handle computed import specifier: ${name}. Only static imports are supported.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const definitions = nameNode.getDefinitionNodes();
|
|
95
|
+
// Filter out the node itself to prevent infinite recursion
|
|
96
|
+
// We compare compilerNode references to handle distinct ts-morph wrapper instances
|
|
97
|
+
let externalDefinition = definitions.find((d) => d.compilerNode !== node.compilerNode);
|
|
98
|
+
|
|
99
|
+
// Fallback: If definition navigation fails (e.g. path aliases), try resolving module manually
|
|
100
|
+
if (!externalDefinition) {
|
|
101
|
+
const importDeclaration = node.getImportDeclaration();
|
|
102
|
+
let sourceFile = importDeclaration.getModuleSpecifierSourceFile();
|
|
103
|
+
|
|
104
|
+
// If ts-morph failed to find the file (common with aliases without project config), try manual lookup
|
|
105
|
+
if (!sourceFile) {
|
|
106
|
+
const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
|
|
107
|
+
const project = node.getProject();
|
|
108
|
+
|
|
109
|
+
if (moduleSpecifier.startsWith('@/')) {
|
|
110
|
+
const suffix = moduleSpecifier.substring(2);
|
|
111
|
+
|
|
112
|
+
// 1. Check if the file is already loaded in the project
|
|
113
|
+
sourceFile = project.getSourceFiles().find((sf) => {
|
|
114
|
+
const filePath = sf.getFilePath();
|
|
115
|
+
// Check for direct match or index file
|
|
116
|
+
return (
|
|
117
|
+
filePath.endsWith(`/${suffix}.ts`) ||
|
|
118
|
+
filePath.endsWith(`/${suffix}.tsx`) ||
|
|
119
|
+
filePath.endsWith(`/${suffix}/index.ts`) ||
|
|
120
|
+
filePath.endsWith(`/${suffix}/index.tsx`)
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 2. If not loaded, try to find and add it from disk (heuristic: @/ -> src/)
|
|
125
|
+
if (!sourceFile) {
|
|
126
|
+
const candidates = [
|
|
127
|
+
`src/${suffix}.ts`,
|
|
128
|
+
`src/${suffix}.tsx`,
|
|
129
|
+
`src/${suffix}/index.ts`,
|
|
130
|
+
`src/${suffix}/index.tsx`,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
try {
|
|
135
|
+
// addSourceFileAtPathIfExists resolves relative to CWD
|
|
136
|
+
const added = project.addSourceFileAtPathIfExists(candidate);
|
|
137
|
+
if (added) {
|
|
138
|
+
sourceFile = added;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore load errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (sourceFile) {
|
|
150
|
+
// Use property path if aliased (import { RealName as Alias }), otherwise name
|
|
151
|
+
// The import specifier is { propertyName as name }
|
|
152
|
+
// If propertyName is undefined, then propertyName is name
|
|
153
|
+
// We want the original propertyName to look up exports
|
|
154
|
+
const localName = node.getName();
|
|
155
|
+
const propertyName = node.compilerNode.propertyName?.getText();
|
|
156
|
+
const exportedName = propertyName ?? localName;
|
|
157
|
+
|
|
158
|
+
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
159
|
+
const declarations = exportedDeclarations.get(exportedName);
|
|
160
|
+
const declaration = declarations?.[0];
|
|
161
|
+
|
|
162
|
+
if (declaration) {
|
|
163
|
+
externalDefinition = declaration;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle re-exports: if the definition is another ImportSpecifier (in a different file), recurse
|
|
169
|
+
if (externalDefinition && externalDefinition.getKind() === SyntaxKind.ImportSpecifier) {
|
|
170
|
+
return staticEval(externalDefinition, context);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!externalDefinition) {
|
|
174
|
+
const importDeclaration = node.getImportDeclaration();
|
|
175
|
+
throw new Error(
|
|
176
|
+
`No definition node found for import specifier '${name}' imported from '${importDeclaration.getModuleSpecifierValue()}'.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return staticEval(externalDefinition, context);
|
|
181
|
+
},
|
|
80
182
|
[SyntaxKind.Identifier]: (node: Identifier, context) => {
|
|
81
183
|
const identifierName = node.getText();
|
|
82
184
|
if (identifierName in KNOWN_IDENTIFIERS) {
|