@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/bin/gqm.cjs +125 -656
  3. package/dist/cjs/index.cjs +2132 -2702
  4. package/dist/esm/migrations/generate.d.ts +1 -9
  5. package/dist/esm/migrations/generate.js +33 -269
  6. package/dist/esm/migrations/generate.js.map +1 -1
  7. package/dist/esm/migrations/index.d.ts +0 -2
  8. package/dist/esm/migrations/index.js +0 -2
  9. package/dist/esm/migrations/index.js.map +1 -1
  10. package/dist/esm/models/model-definitions.d.ts +1 -4
  11. package/dist/esm/resolvers/filters.js +14 -73
  12. package/dist/esm/resolvers/filters.js.map +1 -1
  13. package/dist/esm/resolvers/selects.js +2 -33
  14. package/dist/esm/resolvers/selects.js.map +1 -1
  15. package/dist/esm/resolvers/utils.d.ts +0 -1
  16. package/dist/esm/resolvers/utils.js +0 -22
  17. package/dist/esm/resolvers/utils.js.map +1 -1
  18. package/docs/docs/3-fields.md +0 -149
  19. package/docs/docs/5-migrations.md +1 -9
  20. package/package.json +2 -2
  21. package/src/bin/gqm/gqm.ts +5 -44
  22. package/src/bin/gqm/settings.ts +0 -7
  23. package/src/bin/gqm/static-eval.ts +102 -0
  24. package/src/bin/gqm/utils.ts +0 -1
  25. package/src/migrations/generate.ts +41 -334
  26. package/src/migrations/index.ts +0 -2
  27. package/src/models/model-definitions.ts +1 -4
  28. package/src/resolvers/filters.ts +25 -81
  29. package/src/resolvers/selects.ts +5 -38
  30. package/src/resolvers/utils.ts +0 -32
  31. package/dist/esm/migrations/generate-functions.d.ts +0 -2
  32. package/dist/esm/migrations/generate-functions.js +0 -60
  33. package/dist/esm/migrations/generate-functions.js.map +0 -1
  34. package/dist/esm/migrations/types.d.ts +0 -7
  35. package/dist/esm/migrations/types.js +0 -2
  36. package/dist/esm/migrations/types.js.map +0 -1
  37. package/dist/esm/migrations/update-functions.d.ts +0 -3
  38. package/dist/esm/migrations/update-functions.js +0 -177
  39. package/dist/esm/migrations/update-functions.js.map +0 -1
  40. package/src/bin/gqm/parse-functions.ts +0 -141
  41. package/src/migrations/generate-functions.ts +0 -74
  42. package/src/migrations/types.ts +0 -7
  43. package/src/migrations/update-functions.ts +0 -221
@@ -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.0-next.8",
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.2",
80
+ "esbuild": "0.27.3",
81
81
  "eslint": "9.39.2",
82
82
  "jest": "30.2.0",
83
83
  "mock-knex": "0.4.13",
@@ -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 functionsPath = await getSetting('functionsPath');
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 functionsPath = await getSetting('functionsPath');
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('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('*')
109
+ .command('*', { noHelp: true })
149
110
  .description('Invalid command')
150
- .action((command) => {
151
- console.error(`Invalid command: ${command}\nSee --help for a list of available commands.`);
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
 
@@ -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) {
@@ -9,7 +9,6 @@ export const findDeclarationInFile = (sourceFile: SourceFile, name: string) => {
9
9
  if (!declaration) {
10
10
  throw new Error(`No ${name} declaration`);
11
11
  }
12
-
13
12
  return declaration;
14
13
  };
15
14