@opensaas/stack-cli 0.39.2 → 0.41.0

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 (107) hide show
  1. package/dist/commands/dev.d.ts.map +1 -1
  2. package/dist/commands/dev.js +0 -5
  3. package/dist/commands/dev.js.map +1 -1
  4. package/dist/commands/generate.d.ts +0 -6
  5. package/dist/commands/generate.d.ts.map +1 -1
  6. package/dist/commands/generate.js +4 -26
  7. package/dist/commands/generate.js.map +1 -1
  8. package/dist/commands/init.d.ts +1 -6
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +1 -7
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/mcp.d.ts.map +1 -1
  13. package/dist/commands/mcp.js +0 -2
  14. package/dist/commands/mcp.js.map +1 -1
  15. package/dist/commands/migrate.d.ts +2 -17
  16. package/dist/commands/migrate.d.ts.map +1 -1
  17. package/dist/commands/migrate.js +3 -59
  18. package/dist/commands/migrate.js.map +1 -1
  19. package/dist/generator/context.d.ts +0 -12
  20. package/dist/generator/context.d.ts.map +1 -1
  21. package/dist/generator/context.js +5 -26
  22. package/dist/generator/context.js.map +1 -1
  23. package/dist/generator/extension.d.ts +0 -9
  24. package/dist/generator/extension.d.ts.map +1 -1
  25. package/dist/generator/extension.js +5 -37
  26. package/dist/generator/extension.js.map +1 -1
  27. package/dist/generator/lists.d.ts +2 -5
  28. package/dist/generator/lists.d.ts.map +1 -1
  29. package/dist/generator/lists.js +2 -12
  30. package/dist/generator/lists.js.map +1 -1
  31. package/dist/generator/output-paths.d.ts +2 -12
  32. package/dist/generator/output-paths.d.ts.map +1 -1
  33. package/dist/generator/output-paths.js +2 -12
  34. package/dist/generator/output-paths.js.map +1 -1
  35. package/dist/generator/plugin-types.d.ts +0 -6
  36. package/dist/generator/plugin-types.d.ts.map +1 -1
  37. package/dist/generator/plugin-types.js +3 -28
  38. package/dist/generator/plugin-types.js.map +1 -1
  39. package/dist/generator/prisma-config.d.ts +6 -25
  40. package/dist/generator/prisma-config.d.ts.map +1 -1
  41. package/dist/generator/prisma-config.js +6 -27
  42. package/dist/generator/prisma-config.js.map +1 -1
  43. package/dist/generator/prisma.d.ts +0 -8
  44. package/dist/generator/prisma.d.ts.map +1 -1
  45. package/dist/generator/prisma.js +24 -63
  46. package/dist/generator/prisma.js.map +1 -1
  47. package/dist/generator/types.d.ts +0 -6
  48. package/dist/generator/types.d.ts.map +1 -1
  49. package/dist/generator/types.js +25 -149
  50. package/dist/generator/types.js.map +1 -1
  51. package/dist/mcp/lib/documentation-provider.d.ts +0 -36
  52. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  53. package/dist/mcp/lib/documentation-provider.js +1 -49
  54. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  55. package/dist/mcp/lib/features/catalog.d.ts +0 -15
  56. package/dist/mcp/lib/features/catalog.d.ts.map +1 -1
  57. package/dist/mcp/lib/features/catalog.js +0 -15
  58. package/dist/mcp/lib/features/catalog.js.map +1 -1
  59. package/dist/mcp/lib/generators/feature-generator.d.ts +3 -24
  60. package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -1
  61. package/dist/mcp/lib/generators/feature-generator.js +4 -38
  62. package/dist/mcp/lib/generators/feature-generator.js.map +1 -1
  63. package/dist/mcp/lib/types.d.ts +0 -3
  64. package/dist/mcp/lib/types.d.ts.map +1 -1
  65. package/dist/mcp/lib/types.js +0 -3
  66. package/dist/mcp/lib/types.js.map +1 -1
  67. package/dist/mcp/lib/wizards/migration-wizard.d.ts +0 -70
  68. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -1
  69. package/dist/mcp/lib/wizards/migration-wizard.js +1 -87
  70. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -1
  71. package/dist/mcp/lib/wizards/wizard-engine.d.ts +0 -39
  72. package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -1
  73. package/dist/mcp/lib/wizards/wizard-engine.js +0 -49
  74. package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -1
  75. package/dist/mcp/server/index.d.ts +0 -3
  76. package/dist/mcp/server/index.d.ts.map +1 -1
  77. package/dist/mcp/server/index.js +1 -9
  78. package/dist/mcp/server/index.js.map +1 -1
  79. package/dist/mcp/server/stack-mcp-server.d.ts +0 -42
  80. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  81. package/dist/mcp/server/stack-mcp-server.js +1 -47
  82. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  83. package/dist/migration/generators/migration-generator.d.ts +0 -55
  84. package/dist/migration/generators/migration-generator.d.ts.map +1 -1
  85. package/dist/migration/generators/migration-generator.js +3 -97
  86. package/dist/migration/generators/migration-generator.js.map +1 -1
  87. package/dist/migration/introspectors/index.d.ts +0 -6
  88. package/dist/migration/introspectors/index.d.ts.map +1 -1
  89. package/dist/migration/introspectors/index.js +0 -6
  90. package/dist/migration/introspectors/index.js.map +1 -1
  91. package/dist/migration/introspectors/keystone-introspector.d.ts +0 -27
  92. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -1
  93. package/dist/migration/introspectors/keystone-introspector.js +0 -40
  94. package/dist/migration/introspectors/keystone-introspector.js.map +1 -1
  95. package/dist/migration/introspectors/nextjs-introspector.d.ts +0 -36
  96. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -1
  97. package/dist/migration/introspectors/nextjs-introspector.js +0 -39
  98. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -1
  99. package/dist/migration/introspectors/prisma-introspector.d.ts +0 -30
  100. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -1
  101. package/dist/migration/introspectors/prisma-introspector.js +1 -42
  102. package/dist/migration/introspectors/prisma-introspector.js.map +1 -1
  103. package/dist/migration/types.d.ts +1 -5
  104. package/dist/migration/types.d.ts.map +1 -1
  105. package/dist/migration/types.js +0 -3
  106. package/dist/migration/types.js.map +1 -1
  107. package/package.json +2 -2
@@ -1,49 +1,34 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { resolveListTimestamps } from './prisma.js';
4
- /**
5
- * Map OpenSaas field types to TypeScript types
6
- */
7
4
  function mapFieldTypeToTypeScript(field) {
8
- // Relationships are handled separately
9
5
  if (field.type === 'relationship') {
10
6
  return null;
11
7
  }
12
- // Use field's own TypeScript type generator if available
13
8
  if (field.getTypeScriptType) {
14
9
  const result = field.getTypeScriptType();
15
10
  return result.type;
16
11
  }
17
- // Fallback for fields without generator methods
18
12
  throw new Error(`Field type "${field.type}" does not implement getTypeScriptType method`);
19
13
  }
20
- /**
21
- * Check if a field is optional in the type
22
- */
23
14
  function isFieldOptional(field) {
24
15
  // Relationships are always nullable
25
16
  if (field.type === 'relationship') {
26
17
  return true;
27
18
  }
28
- // Use field's own TypeScript type generator if available
29
19
  if (field.getTypeScriptType) {
30
20
  const result = field.getTypeScriptType();
31
21
  return result.optional;
32
22
  }
33
- // Fallback: assume optional
34
23
  return true;
35
24
  }
36
- /**
37
- * Get names of virtual fields in a list
38
- */
39
25
  function getVirtualFieldNames(fields) {
40
26
  return Object.entries(fields)
41
27
  .filter(([_, config]) => config.type === 'virtual')
42
28
  .map(([name, _]) => name);
43
29
  }
44
30
  /**
45
- * Generate virtual fields type - only contains virtual fields
46
- * This is intersected with Prisma's GetPayload to add virtual fields to query results
31
+ * Intersected with Prisma's GetPayload to add virtual fields to query results.
47
32
  */
48
33
  function generateVirtualFieldsType(listName, fields) {
49
34
  const lines = [];
@@ -61,7 +46,6 @@ function generateVirtualFieldsType(listName, fields) {
61
46
  const nullability = optional ? ' | null' : '';
62
47
  lines.push(` ${fieldName}: ${tsType}${nullability}`);
63
48
  }
64
- // If no virtual fields, make it an empty object
65
49
  if (virtualFields.length === 0) {
66
50
  lines.push(' // No virtual fields defined');
67
51
  }
@@ -69,8 +53,7 @@ function generateVirtualFieldsType(listName, fields) {
69
53
  return lines.join('\n');
70
54
  }
71
55
  /**
72
- * Generate transformed fields type - fields with resultExtension transformations
73
- * This replaces Prisma's base types with transformed types (e.g., string -> HashedPassword)
56
+ * Replaces Prisma's base types with transformed types (e.g., string -> HashedPassword).
74
57
  */
75
58
  function generateTransformedFieldsType(listName, fields) {
76
59
  const lines = [];
@@ -87,7 +70,6 @@ function generateTransformedFieldsType(listName, fields) {
87
70
  lines.push(` ${fieldName}: ${fieldConfig.resultExtension.outputType}${nullability}`);
88
71
  }
89
72
  }
90
- // If no transformed fields, make it an empty object
91
73
  if (transformedFields.length === 0) {
92
74
  lines.push(' // No transformed fields defined');
93
75
  }
@@ -95,8 +77,8 @@ function generateTransformedFieldsType(listName, fields) {
95
77
  return lines.join('\n');
96
78
  }
97
79
  /**
98
- * Generate TypeScript Output type for a model (includes virtual fields)
99
- * This is kept for backwards compatibility but CustomDB uses Prisma's GetPayload + VirtualFields
80
+ * Kept for backwards compatibility `CustomDB` itself uses Prisma's
81
+ * GetPayload + VirtualFields instead.
100
82
  */
101
83
  function generateModelOutputType(listName,
102
84
  // ListConfig is generic over per-list TypeInfo; we only read `db`/`fields`, which are
@@ -110,7 +92,6 @@ listConfig, dbConfig, isSingleton) {
110
92
  // Keystone 6, so the TypeScript id type is `number` rather than `string`.
111
93
  lines.push(isSingleton ? ' id: number' : ' id: string');
112
94
  for (const [fieldName, fieldConfig] of Object.entries(fields)) {
113
- // Skip virtual fields - they're in VirtualFields type
114
95
  if (fieldConfig.type === 'virtual')
115
96
  continue;
116
97
  if (fieldConfig.type === 'relationship') {
@@ -127,7 +108,7 @@ listConfig, dbConfig, isSingleton) {
127
108
  else {
128
109
  const tsType = mapFieldTypeToTypeScript(fieldConfig);
129
110
  if (!tsType)
130
- continue; // Skip if no type returned
111
+ continue;
131
112
  const optional = isFieldOptional(fieldConfig);
132
113
  const nullability = optional ? ' | null' : '';
133
114
  lines.push(` ${fieldName}: ${tsType}${nullability}`);
@@ -142,12 +123,9 @@ listConfig, dbConfig, isSingleton) {
142
123
  if (timestamps.updatedAt) {
143
124
  lines.push(' updatedAt: Date');
144
125
  }
145
- lines.push('} & ' + listName + 'VirtualFields'); // Include virtual fields
126
+ lines.push('} & ' + listName + 'VirtualFields');
146
127
  return lines.join('\n');
147
128
  }
148
- /**
149
- * Generate convenience type alias (List = ListOutput)
150
- */
151
129
  function generateModelTypeAlias(listName) {
152
130
  return `export type ${listName} = ${listName}Output`;
153
131
  }
@@ -194,15 +172,11 @@ function renderScalarInputMember(fieldName, fieldConfig, forCreate, indent) {
194
172
  }
195
173
  return `${indent}${fieldName}${optional}: ${tsType}${nullability}`;
196
174
  }
197
- /**
198
- * Generate CreateInput type
199
- */
200
175
  function generateCreateInputType(listName, fields) {
201
176
  const lines = [];
202
177
  lines.push(`export type ${listName}CreateInput = {`);
203
178
  for (const [fieldName, fieldConfig] of Object.entries(fields)) {
204
- // Skip virtual fields - they don't accept input in create operations
205
- // Virtual fields with resolveInput hooks handle side effects but don't store data
179
+ // Virtual fields with resolveInput hooks can handle side effects but don't store data.
206
180
  if (fieldConfig.virtual) {
207
181
  continue;
208
182
  }
@@ -220,7 +194,7 @@ function generateCreateInputType(listName, fields) {
220
194
  // optionality + `| null` nullability for nullable scalars.
221
195
  const member = renderScalarInputMember(fieldName, fieldConfig, true, ' ');
222
196
  if (!member)
223
- continue; // Skip if no type returned
197
+ continue;
224
198
  lines.push(member);
225
199
  }
226
200
  }
@@ -358,9 +332,6 @@ function buildWriteInputOverride(prismaBase, fields, forCreate) {
358
332
  }
359
333
  return `${base} & {\n${members.join('\n')}\n }`;
360
334
  }
361
- /**
362
- * Generate UpdateInput type
363
- */
364
335
  function generateUpdateInputType(listName, fields) {
365
336
  const lines = [];
366
337
  lines.push(`export type ${listName}UpdateInput = {`);
@@ -384,25 +355,16 @@ function generateUpdateInputType(listName, fields) {
384
355
  // update (always optional) with `| null` nullability for nullable scalars.
385
356
  const member = renderScalarInputMember(fieldName, fieldConfig, false, ' ');
386
357
  if (!member)
387
- continue; // Skip if no type returned
358
+ continue;
388
359
  lines.push(member);
389
360
  }
390
361
  }
391
362
  lines.push('}');
392
363
  return lines.join('\n');
393
364
  }
394
- /**
395
- * Generate WhereInput type by re-exporting Prisma's native WhereInput
396
- * This ensures compatibility with all Prisma versions and includes all filter operators
397
- */
398
365
  function generateWhereInputType(listName, _fields) {
399
- // Simply re-export Prisma's generated WhereInput type
400
- // Prisma already generates comprehensive WhereInput types with all filter operators
401
366
  return `export type ${listName}WhereInput = Prisma.${listName}WhereInput`;
402
367
  }
403
- /**
404
- * Generate hook types that reference Prisma input types
405
- */
406
368
  function generateHookTypes(listName) {
407
369
  const lines = [];
408
370
  lines.push(`/**`);
@@ -446,10 +408,6 @@ function generateHookTypes(listName) {
446
408
  lines.push(`}`);
447
409
  return lines.join('\n');
448
410
  }
449
- /**
450
- * Generate Select type that includes virtual fields
451
- * Extends Prisma's Select type with virtual field selection support and nested relationship overrides
452
- */
453
411
  function generateSelectType(listName, fields) {
454
412
  const virtualFields = getVirtualFieldNames(fields);
455
413
  const relationshipFields = Object.entries(fields)
@@ -459,7 +417,6 @@ function generateSelectType(listName, fields) {
459
417
  targetList: config.ref.split('.')[0],
460
418
  }));
461
419
  if (virtualFields.length === 0 && relationshipFields.length === 0) {
462
- // No virtual fields and no relationships - just re-export Prisma type
463
420
  return `/**
464
421
  * Select type for ${listName}
465
422
  * No virtual fields defined, uses Prisma's Select type directly
@@ -467,13 +424,11 @@ function generateSelectType(listName, fields) {
467
424
  export type ${listName}Select = Prisma.${listName}Select`;
468
425
  }
469
426
  const lines = [];
470
- // Add virtual field properties
471
427
  if (virtualFields.length > 0) {
472
428
  virtualFields.forEach((name) => {
473
429
  lines.push(` ${name}?: boolean`);
474
430
  });
475
431
  }
476
- // Override relationship properties to use custom DefaultArgs
477
432
  if (relationshipFields.length > 0) {
478
433
  relationshipFields.forEach(({ name, targetList }) => {
479
434
  lines.push(` ${name}?: boolean | ${targetList}DefaultArgs`);
@@ -508,38 +463,30 @@ ${lines.join('\n')}
508
463
  }`;
509
464
  }
510
465
  /**
511
- * Generate Include type that includes virtual fields
512
- * Extends Prisma's Include type with virtual field inclusion support and nested relationship overrides
513
- * Note: Only generates Include type if the list has relationship fields,
514
- * since Prisma only generates Include types for models with relations
466
+ * Only generates an Include type if the list has relationship fields — Prisma
467
+ * itself only generates Include types for models with relations.
515
468
  */
516
469
  function generateIncludeType(listName, fields) {
517
- // Check if list has any relationship fields
518
470
  const relationshipFields = Object.entries(fields)
519
471
  .filter(([_, config]) => config.type === 'relationship')
520
472
  .map(([name, config]) => ({
521
473
  name,
522
474
  targetList: config.ref.split('.')[0],
523
475
  }));
524
- // Prisma only generates Include types for models with relationships
525
- // If there are no relationships, don't generate an Include type
526
476
  if (relationshipFields.length === 0) {
527
477
  return null;
528
478
  }
529
479
  const virtualFields = getVirtualFieldNames(fields);
530
480
  const lines = [];
531
- // Add virtual field properties
532
481
  if (virtualFields.length > 0) {
533
482
  virtualFields.forEach((name) => {
534
483
  lines.push(` ${name}?: boolean`);
535
484
  });
536
485
  }
537
- // Override relationship properties to use custom DefaultArgs
538
486
  relationshipFields.forEach(({ name, targetList }) => {
539
487
  lines.push(` ${name}?: boolean | ${targetList}DefaultArgs`);
540
488
  });
541
489
  if (lines.length === 0) {
542
- // No virtual fields - just re-export Prisma type
543
490
  return `/**
544
491
  * Include type for ${listName}
545
492
  * No virtual fields defined, uses Prisma's Include type directly
@@ -567,10 +514,9 @@ ${lines.join('\n')}
567
514
  }`;
568
515
  }
569
516
  /**
570
- * Generate GetPayload helper type that adds virtual fields support to Prisma's GetPayload
571
- * This allows users to use Prisma.{ListName}GetPayload<T> pattern with virtual fields
572
- * Always generated to ensure consistency in CustomDB type signatures
573
- * Even lists without virtual fields need this to support nested relations with virtual fields
517
+ * Always generated, even for lists without virtual fields CustomDB type
518
+ * signatures stay consistent, and lists without their own virtual fields
519
+ * still need this to support nested relations that have them.
574
520
  */
575
521
  function generateGetPayloadType(listName, fields) {
576
522
  const virtualFields = getVirtualFieldNames(fields);
@@ -582,7 +528,6 @@ function generateGetPayloadType(listName, fields) {
582
528
  // payload: only the assembled logical field (added back via TransformedFields)
583
529
  // is exposed. Collect those raw column names for omission. See ADR-0006.
584
530
  const multiColumnRawNames = Object.entries(fields).flatMap(([name, config]) => config.getColumnNames ? config.getColumnNames(name) : []);
585
- // Get relationship fields to override with custom GetPayload types
586
531
  const relationshipFields = Object.entries(fields)
587
532
  .filter(([_, config]) => config.type === 'relationship')
588
533
  .map(([name, config]) => ({
@@ -591,7 +536,6 @@ function generateGetPayloadType(listName, fields) {
591
536
  many: !!config.many,
592
537
  }));
593
538
  const lines = [];
594
- // Build documentation
595
539
  lines.push(`/**`);
596
540
  if (virtualFields.length > 0 || transformedFieldNames.length > 0) {
597
541
  lines.push(` * GetPayload type for ${listName} with virtual and transformed field support`);
@@ -634,11 +578,9 @@ function generateGetPayloadType(listName, fields) {
634
578
  else {
635
579
  lines.push(` Prisma.${listName}GetPayload<${prismaT}> &`);
636
580
  }
637
- // Add transformed fields back
638
581
  if (transformedFieldNames.length > 0) {
639
582
  lines.push(` ${listName}TransformedFields &`);
640
583
  }
641
- // Add relationship fields back with custom GetPayload types
642
584
  if (relationshipFields.length > 0) {
643
585
  lines.push(` {`);
644
586
  for (const rel of relationshipFields) {
@@ -669,7 +611,6 @@ function generateGetPayloadType(listName, fields) {
669
611
  }
670
612
  lines.push(` } &`);
671
613
  }
672
- // Build the virtual fields conditional type
673
614
  if (virtualFields.length > 0) {
674
615
  lines.push(` (`);
675
616
  lines.push(` T extends { select: any }`);
@@ -696,15 +637,10 @@ function generateGetPayloadType(listName, fields) {
696
637
  lines.push(` )`);
697
638
  }
698
639
  else {
699
- // No virtual fields, just use empty object
700
640
  lines.push(` {}`);
701
641
  }
702
642
  return lines.join('\n');
703
643
  }
704
- /**
705
- * Generate DefaultArgs type for nested relationship selections
706
- * This type is used when selecting relationships to enable custom Select/Include types
707
- */
708
644
  function generateDefaultArgsType(listName, fields) {
709
645
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
710
646
  if (hasRelationships) {
@@ -727,9 +663,6 @@ export type ${listName}DefaultArgs = {
727
663
  }`;
728
664
  }
729
665
  }
730
- /**
731
- * Generate custom FindUniqueArgs type that uses our extended Select/Include
732
- */
733
666
  function generateFindUniqueArgsType(listName, fields) {
734
667
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
735
668
  if (hasRelationships) {
@@ -752,9 +685,6 @@ export type ${listName}FindUniqueArgs = Omit<Prisma.${listName}FindUniqueArgs, '
752
685
  }`;
753
686
  }
754
687
  }
755
- /**
756
- * Generate custom FindManyArgs type that uses our extended Select/Include
757
- */
758
688
  function generateFindManyArgsType(listName, fields) {
759
689
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
760
690
  if (hasRelationships) {
@@ -777,9 +707,6 @@ export type ${listName}FindManyArgs = Omit<Prisma.${listName}FindManyArgs, 'sele
777
707
  }`;
778
708
  }
779
709
  }
780
- /**
781
- * Generate custom FindFirstArgs type that uses our extended Select/Include
782
- */
783
710
  function generateFindFirstArgsType(listName, fields) {
784
711
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
785
712
  if (hasRelationships) {
@@ -803,9 +730,8 @@ export type ${listName}FindFirstArgs = Omit<Prisma.${listName}FindFirstArgs, 'se
803
730
  }
804
731
  }
805
732
  /**
806
- * Generate custom GetArgs type for a singleton list's `get()` method — the same
807
- * include/query narrowing as FindUniqueArgs, minus `where` (a singleton has no
808
- * unique selector; there is exactly one row).
733
+ * The same include/query narrowing as FindUniqueArgs, minus `where` (a
734
+ * singleton has no unique selector; there is exactly one row).
809
735
  */
810
736
  function generateGetArgsType(listName, fields) {
811
737
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
@@ -829,9 +755,6 @@ export type ${listName}GetArgs = {
829
755
  }`;
830
756
  }
831
757
  }
832
- /**
833
- * Generate custom CreateArgs type that uses our extended Select/Include
834
- */
835
758
  function generateCreateArgsType(listName, fields) {
836
759
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
837
760
  // Narrow scalar `data` members to OpenSaaS types so field-level narrowing
@@ -860,9 +783,6 @@ export type ${listName}CreateArgs = Omit<Prisma.${listName}CreateArgs, 'select'
860
783
  }`;
861
784
  }
862
785
  }
863
- /**
864
- * Generate custom UpdateArgs type that uses our extended Select/Include
865
- */
866
786
  function generateUpdateArgsType(listName, fields) {
867
787
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
868
788
  // See generateCreateArgsType: narrow scalar `data` members (#599).
@@ -889,9 +809,6 @@ export type ${listName}UpdateArgs = Omit<Prisma.${listName}UpdateArgs, 'select'
889
809
  }`;
890
810
  }
891
811
  }
892
- /**
893
- * Generate custom DeleteArgs type that uses our extended Select/Include
894
- */
895
812
  function generateDeleteArgsType(listName, fields) {
896
813
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
897
814
  if (hasRelationships) {
@@ -912,9 +829,6 @@ export type ${listName}DeleteArgs = Omit<Prisma.${listName}DeleteArgs, 'select'>
912
829
  }`;
913
830
  }
914
831
  }
915
- /**
916
- * Generate custom CreateManyArgs type that uses our extended Select/Include
917
- */
918
832
  function generateCreateManyArgsType(listName, fields) {
919
833
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
920
834
  // createMany `data` is an array of scalar inputs; narrow each element (#599).
@@ -941,9 +855,6 @@ export type ${listName}CreateManyArgs = {
941
855
  }`;
942
856
  }
943
857
  }
944
- /**
945
- * Generate custom UpdateManyArgs type that uses our extended Select/Include
946
- */
947
858
  function generateUpdateManyArgsType(listName, fields) {
948
859
  const hasRelationships = Object.values(fields).some((field) => field.type === 'relationship');
949
860
  // updateMany `data` is a single partial scalar input; narrow it (#599).
@@ -980,6 +891,10 @@ export type ${listName}UpdateManyArgs = {
980
891
  * intersection, combined with the self-referential `Context.sudo(): Context`
981
892
  * return type, hit `TS2589: Type instantiation is excessively deep` once a
982
893
  * schema grew past ~7-8 lists.
894
+ *
895
+ * Every method below is generic over its own `*Args` type, to preserve
896
+ * Prisma's conditional return type together with the virtual-field-aware
897
+ * `GetPayload` — `count` is the only exception, since it returns a plain number.
983
898
  */
984
899
  function generateListCrudInterface(listName, isSingleton) {
985
900
  const lines = [];
@@ -987,37 +902,28 @@ function generateListCrudInterface(listName, isSingleton) {
987
902
  lines.push(` * Access-controlled CRUD methods for ${listName}, with virtual field support.`);
988
903
  lines.push(` */`);
989
904
  lines.push(`export interface ${listName}Crud {`);
990
- // findUnique - generic to preserve Prisma's conditional return type with custom Args for virtual field support
991
905
  lines.push(` findUnique: <T extends ${listName}FindUniqueArgs>(`);
992
906
  lines.push(` args: Prisma.SelectSubset<T, ${listName}FindUniqueArgs>`);
993
907
  lines.push(` ) => Promise<${listName}GetPayload<T> | null>`);
994
- // findFirst - generic to preserve Prisma's conditional return type with custom Args for virtual field support
995
908
  lines.push(` findFirst: <T extends ${listName}FindFirstArgs>(`);
996
909
  lines.push(` args?: Prisma.SelectSubset<T, ${listName}FindFirstArgs>`);
997
910
  lines.push(` ) => Promise<${listName}GetPayload<T> | null>`);
998
- // findMany - generic to preserve Prisma's conditional return type with custom Args for virtual field support
999
911
  lines.push(` findMany: <T extends ${listName}FindManyArgs>(`);
1000
912
  lines.push(` args?: Prisma.SelectSubset<T, ${listName}FindManyArgs>`);
1001
913
  lines.push(` ) => Promise<Array<${listName}GetPayload<T>>>`);
1002
- // create - generic to preserve Prisma's conditional return type with custom Args for virtual field support
1003
914
  lines.push(` create: <T extends ${listName}CreateArgs>(`);
1004
915
  lines.push(` args: Prisma.SelectSubset<T, ${listName}CreateArgs>`);
1005
916
  lines.push(` ) => Promise<${listName}GetPayload<T>>`);
1006
- // update - generic to preserve Prisma's conditional return type with custom Args for virtual field support
1007
917
  lines.push(` update: <T extends ${listName}UpdateArgs>(`);
1008
918
  lines.push(` args: Prisma.SelectSubset<T, ${listName}UpdateArgs>`);
1009
919
  lines.push(` ) => Promise<${listName}GetPayload<T> | null>`);
1010
- // delete - generic to preserve Prisma's conditional return type with custom Args for virtual field support
1011
920
  lines.push(` delete: <T extends ${listName}DeleteArgs>(`);
1012
921
  lines.push(` args: Prisma.SelectSubset<T, ${listName}DeleteArgs>`);
1013
922
  lines.push(` ) => Promise<${listName}GetPayload<T> | null>`);
1014
- // count - no changes to return type
1015
923
  lines.push(` count: (args?: Prisma.${listName}CountArgs) => Promise<number>`);
1016
- // createMany - generic to preserve Prisma's conditional return type with custom Args for virtual field support
1017
924
  lines.push(` createMany: <T extends ${listName}CreateManyArgs>(`);
1018
925
  lines.push(` args: Prisma.SelectSubset<T, ${listName}CreateManyArgs>`);
1019
926
  lines.push(` ) => Promise<Array<${listName}GetPayload<T>>>`);
1020
- // updateMany - generic to preserve Prisma's conditional return type with custom Args for virtual field support
1021
927
  lines.push(` updateMany: <T extends ${listName}UpdateManyArgs>(`);
1022
928
  lines.push(` args: Prisma.SelectSubset<T, ${listName}UpdateManyArgs>`);
1023
929
  lines.push(` ) => Promise<Array<${listName}GetPayload<T>>>`);
@@ -1043,7 +949,7 @@ function generateListCrudInterface(listName, isSingleton) {
1043
949
  */
1044
950
  function generateCustomDBType(config) {
1045
951
  const lines = [];
1046
- // Generate list of db keys to omit from AccessControlledDB
952
+ // db keys to omit from AccessControlledDB before re-adding each list's own {List}Crud.
1047
953
  const dbKeys = Object.keys(config.lists).map((listName) => {
1048
954
  const dbKey = listName.charAt(0).toLowerCase() + listName.slice(1);
1049
955
  return `'${dbKey}'`;
@@ -1082,7 +988,6 @@ function generateCustomDBType(config) {
1082
988
  */
1083
989
  function generateContextType(_config) {
1084
990
  const lines = [];
1085
- // Generate BaseContext - minimal interface for services that only need db and session
1086
991
  lines.push('/**');
1087
992
  lines.push(' * Base context type for services that only need database and session access');
1088
993
  lines.push(' * Compatible with both AccessContext (from hooks) and Context (from server actions)');
@@ -1093,41 +998,33 @@ function generateContextType(_config) {
1093
998
  lines.push(' session: TSession');
1094
999
  lines.push('}');
1095
1000
  lines.push('');
1096
- // Generate Context - extends BaseContext and adds server action capabilities
1097
1001
  lines.push('/**');
1098
1002
  lines.push(' * Full context type with server action capabilities and virtual field typing');
1099
- lines.push(' * Extends BaseContext and adds serverAction and sudo methods');
1003
+ lines.push(' * Extends BaseContext and adds serverAction, sudo, and withSession methods');
1100
1004
  lines.push(' * Use this type in server actions and components that need full context capabilities');
1101
1005
  lines.push(' */');
1102
1006
  lines.push('export interface Context<TSession extends OpensaasSession = OpensaasSession> extends BaseContext<TSession> {');
1103
1007
  lines.push(' serverAction: (props: ServerActionProps) => Promise<unknown>');
1104
1008
  lines.push(' sudo: () => Context<TSession>');
1009
+ lines.push(' withSession: (session: TSession | null) => Context<TSession>');
1105
1010
  lines.push('}');
1106
1011
  return lines.join('\n');
1107
1012
  }
1108
- /**
1109
- * Collect TypeScript imports from field configurations
1110
- */
1111
1013
  function collectFieldImports(config) {
1112
1014
  const importsMap = new Map();
1113
- // Iterate through all lists and fields
1114
1015
  for (const listConfig of Object.values(config.lists)) {
1115
1016
  for (const fieldConfig of Object.values(listConfig.fields)) {
1116
- // Check if field provides imports
1117
1017
  if (fieldConfig.getTypeScriptImports) {
1118
1018
  const imports = fieldConfig.getTypeScriptImports();
1119
1019
  for (const imp of imports) {
1120
1020
  const existing = importsMap.get(imp.from);
1121
1021
  if (existing) {
1122
- // Merge names into existing import
1123
1022
  imp.names.forEach((name) => existing.names.add(name));
1124
- // If either import is not type-only, make the merged import not type-only
1125
1023
  if (imp.typeOnly === false) {
1126
1024
  existing.typeOnly = false;
1127
1025
  }
1128
1026
  }
1129
1027
  else {
1130
- // Add new import
1131
1028
  importsMap.set(imp.from, {
1132
1029
  names: new Set(imp.names),
1133
1030
  typeOnly: imp.typeOnly ?? true,
@@ -1137,25 +1034,19 @@ function collectFieldImports(config) {
1137
1034
  }
1138
1035
  }
1139
1036
  }
1140
- // Convert map to array
1141
1037
  return Array.from(importsMap.entries()).map(([from, { names, typeOnly }]) => ({
1142
1038
  names: Array.from(names).sort(),
1143
1039
  from,
1144
1040
  typeOnly,
1145
1041
  }));
1146
1042
  }
1147
- /**
1148
- * Generate all TypeScript types from config
1149
- */
1150
1043
  export function generateTypes(config) {
1151
1044
  const lines = [];
1152
- // Add header comment
1153
1045
  lines.push('/**');
1154
1046
  lines.push(' * Generated types from OpenSaas configuration');
1155
1047
  lines.push(' * DO NOT EDIT - This file is automatically generated');
1156
1048
  lines.push(' */');
1157
1049
  lines.push('');
1158
- // Add necessary imports
1159
1050
  // Use alias for Session to avoid conflicts if user has a list named "Session".
1160
1051
  // Session and AccessContext stay on the public root entry point (Session is the
1161
1052
  // module-augmentation target); the rest are unstable runtime plumbing on /internal.
@@ -1166,7 +1057,6 @@ export function generateTypes(config) {
1166
1057
  // `extensionAlias`. See ADR-0008.
1167
1058
  lines.push("import type { PrismaClient, Prisma } from './prisma-client/client.ts'");
1168
1059
  lines.push("import type { PluginServices } from './plugin-types.ts'");
1169
- // Add field-specific imports
1170
1060
  const fieldImports = collectFieldImports(config);
1171
1061
  for (const imp of fieldImports) {
1172
1062
  const typePrefix = imp.typeOnly ? 'type ' : '';
@@ -1174,8 +1064,6 @@ export function generateTypes(config) {
1174
1064
  lines.push(`import ${typePrefix}{ ${names} } from '${imp.from}'`);
1175
1065
  }
1176
1066
  lines.push('');
1177
- // Emit shared utility type used by GetPayload types to strip virtual field keys
1178
- // from select/include before passing to Prisma's GetPayload (prevents `never` intersection)
1179
1067
  lines.push('// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them');
1180
1068
  lines.push('type StripVirtualFromArgs<T, VirtualKeys extends string> =');
1181
1069
  lines.push(' T extends { select: infer S extends object }');
@@ -1184,12 +1072,11 @@ export function generateTypes(config) {
1184
1072
  lines.push(" ? Omit<T, 'include'> & { include: Omit<I, VirtualKeys> }");
1185
1073
  lines.push(' : T');
1186
1074
  lines.push('');
1187
- // Generate types for each list
1188
1075
  for (const [listName, listConfig] of Object.entries(config.lists)) {
1189
- // Generate VirtualFields type first (needed by Output type and CustomDB)
1076
+ // VirtualFields first needed by the Output type and CustomDB.
1190
1077
  lines.push(generateVirtualFieldsType(listName, listConfig.fields));
1191
1078
  lines.push('');
1192
- // Generate TransformedFields type (needed by CustomDB)
1079
+ // TransformedFields needed by CustomDB.
1193
1080
  lines.push(generateTransformedFieldsType(listName, listConfig.fields));
1194
1081
  lines.push('');
1195
1082
  lines.push(generateModelOutputType(listName, listConfig, config.db, !!listConfig.isSingleton));
@@ -1204,22 +1091,17 @@ export function generateTypes(config) {
1204
1091
  lines.push('');
1205
1092
  lines.push(generateHookTypes(listName));
1206
1093
  lines.push('');
1207
- // Generate Select and Include types with virtual field support
1208
1094
  lines.push(generateSelectType(listName, listConfig.fields));
1209
1095
  lines.push('');
1210
- // Only generate Include type if the list has relationships
1211
1096
  const includeType = generateIncludeType(listName, listConfig.fields);
1212
1097
  if (includeType) {
1213
1098
  lines.push(includeType);
1214
1099
  lines.push('');
1215
1100
  }
1216
- // Generate GetPayload helper type with virtual field support
1217
1101
  lines.push(generateGetPayloadType(listName, listConfig.fields));
1218
1102
  lines.push('');
1219
- // Generate DefaultArgs type for nested relationship support
1220
1103
  lines.push(generateDefaultArgsType(listName, listConfig.fields));
1221
1104
  lines.push('');
1222
- // Generate custom Args types with virtual field support
1223
1105
  lines.push(generateFindUniqueArgsType(listName, listConfig.fields));
1224
1106
  lines.push('');
1225
1107
  lines.push(generateFindManyArgsType(listName, listConfig.fields));
@@ -1241,19 +1123,13 @@ export function generateTypes(config) {
1241
1123
  lines.push(generateUpdateManyArgsType(listName, listConfig.fields));
1242
1124
  lines.push('');
1243
1125
  }
1244
- // Generate CustomDB interface
1245
1126
  lines.push(generateCustomDBType(config));
1246
1127
  lines.push('');
1247
- // Generate Context type
1248
1128
  lines.push(generateContextType(config));
1249
1129
  return lines.join('\n');
1250
1130
  }
1251
- /**
1252
- * Write TypeScript types to file
1253
- */
1254
1131
  export function writeTypes(config, outputPath) {
1255
1132
  const types = generateTypes(config);
1256
- // Ensure directory exists
1257
1133
  const dir = path.dirname(outputPath);
1258
1134
  if (!fs.existsSync(dir)) {
1259
1135
  fs.mkdirSync(dir, { recursive: true });