@opensaas/stack-core 0.1.7 → 0.4.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 (66) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +352 -0
  3. package/CLAUDE.md +46 -1
  4. package/dist/access/engine.d.ts +7 -6
  5. package/dist/access/engine.d.ts.map +1 -1
  6. package/dist/access/engine.js +55 -0
  7. package/dist/access/engine.js.map +1 -1
  8. package/dist/access/engine.test.d.ts +2 -0
  9. package/dist/access/engine.test.d.ts.map +1 -0
  10. package/dist/access/engine.test.js +125 -0
  11. package/dist/access/engine.test.js.map +1 -0
  12. package/dist/access/types.d.ts +39 -9
  13. package/dist/access/types.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +40 -20
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +34 -15
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/config/plugin-engine.d.ts.map +1 -1
  19. package/dist/config/plugin-engine.js +9 -0
  20. package/dist/config/plugin-engine.js.map +1 -1
  21. package/dist/config/types.d.ts +277 -84
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/context/index.d.ts +5 -3
  24. package/dist/context/index.d.ts.map +1 -1
  25. package/dist/context/index.js +146 -20
  26. package/dist/context/index.js.map +1 -1
  27. package/dist/context/nested-operations.d.ts.map +1 -1
  28. package/dist/context/nested-operations.js +88 -72
  29. package/dist/context/nested-operations.js.map +1 -1
  30. package/dist/fields/index.d.ts +65 -9
  31. package/dist/fields/index.d.ts.map +1 -1
  32. package/dist/fields/index.js +98 -16
  33. package/dist/fields/index.js.map +1 -1
  34. package/dist/hooks/index.d.ts +28 -12
  35. package/dist/hooks/index.d.ts.map +1 -1
  36. package/dist/hooks/index.js +16 -0
  37. package/dist/hooks/index.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/mcp/handler.js +1 -0
  42. package/dist/mcp/handler.js.map +1 -1
  43. package/dist/validation/schema.d.ts.map +1 -1
  44. package/dist/validation/schema.js +4 -2
  45. package/dist/validation/schema.js.map +1 -1
  46. package/package.json +8 -9
  47. package/src/access/engine.test.ts +145 -0
  48. package/src/access/engine.ts +73 -9
  49. package/src/access/types.ts +38 -8
  50. package/src/config/index.ts +45 -23
  51. package/src/config/plugin-engine.ts +13 -3
  52. package/src/config/types.ts +347 -117
  53. package/src/context/index.ts +176 -23
  54. package/src/context/nested-operations.ts +83 -71
  55. package/src/fields/index.ts +132 -27
  56. package/src/hooks/index.ts +63 -20
  57. package/src/index.ts +9 -0
  58. package/src/mcp/handler.ts +2 -1
  59. package/src/validation/schema.ts +4 -2
  60. package/tests/context.test.ts +38 -6
  61. package/tests/field-types.test.ts +729 -0
  62. package/tests/password-type-distribution.test.ts +0 -1
  63. package/tests/password-types.test.ts +0 -1
  64. package/tests/plugin-engine.test.ts +1102 -0
  65. package/tests/sudo.test.ts +230 -2
  66. package/tsconfig.tsbuildinfo +1 -1
@@ -18,12 +18,24 @@ export type FieldType =
18
18
  * Field-level hooks for data transformation and side effects
19
19
  * Allows field types to define custom behavior during operations
20
20
  *
21
- * @template TInput - Type of the input value (what goes into the database)
22
- * @template TOutput - Type of the output value (what comes out of the database)
23
- * @template TItem - Type of the parent item/record
21
+ * @template TTypeInfo - List type information including item and input types
22
+ * @template TFieldKey - The specific field name (defaults to any field in the list)
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // For a 'title' field on Post list:
27
+ * FieldHooks<Lists.Post.TypeInfo, 'title'>
28
+ * // resolveOutput returns: string | undefined (field-specific)
29
+ *
30
+ * // Generic (for field builders):
31
+ * FieldHooks<TTypeInfo>
32
+ * // resolveOutput returns: union of all field types
33
+ * ```
24
34
  */
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
35
+ export type FieldHooks<
36
+ TTypeInfo extends TypeInfo,
37
+ TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
38
+ > = {
27
39
  /**
28
40
  * Transform field value before database write
29
41
  * Called during create/update operations after list-level resolveInput but before validation
@@ -41,12 +53,15 @@ export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
41
53
  */
42
54
  resolveInput?: (args: {
43
55
  operation: 'create' | 'update'
44
- inputValue: TInput | undefined
45
- item?: TItem
56
+ inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
57
+ item?: TTypeInfo['item']
46
58
  listKey: string
47
- fieldName: string
59
+ fieldName: TFieldKey
48
60
  context: import('../access/types.js').AccessContext
49
- }) => Promise<TInput | undefined> | TInput | undefined
61
+ }) =>
62
+ | Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined>
63
+ | GetFieldValueType<TTypeInfo['fields'], TFieldKey>
64
+ | undefined
50
65
 
51
66
  /**
52
67
  * Perform side effects before database write
@@ -63,10 +78,10 @@ export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
63
78
  */
64
79
  beforeOperation?: (args: {
65
80
  operation: 'create' | 'update' | 'delete'
66
- resolvedValue: TInput | undefined
67
- item?: TItem
81
+ resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
82
+ item?: TTypeInfo['item']
68
83
  listKey: string
69
- fieldName: string
84
+ fieldName: TFieldKey
70
85
  context: import('../access/types.js').AccessContext
71
86
  }) => Promise<void> | void
72
87
 
@@ -83,25 +98,14 @@ export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
83
98
  * }
84
99
  * ```
85
100
  */
86
- afterOperation?: (
87
- args:
88
- | {
89
- operation: 'create' | 'update' | 'delete'
90
- value: TInput | undefined
91
- item: TItem
92
- listKey: string
93
- fieldName: string
94
- context: import('../access/types.js').AccessContext
95
- }
96
- | {
97
- operation: 'query'
98
- value: TOutput | undefined
99
- item: TItem
100
- listKey: string
101
- fieldName: string
102
- context: import('../access/types.js').AccessContext
103
- },
104
- ) => Promise<void> | void
101
+ afterOperation?: (args: {
102
+ operation: 'create' | 'update' | 'delete' | 'query'
103
+ value: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
104
+ item: TTypeInfo['item']
105
+ listKey: string
106
+ fieldName: TFieldKey
107
+ context: import('../access/types.js').AccessContext
108
+ }) => Promise<void> | void
105
109
 
106
110
  /**
107
111
  * Transform field value after database read
@@ -120,45 +124,57 @@ export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
120
124
  */
121
125
  resolveOutput?: (args: {
122
126
  operation: 'query'
123
- value: TInput | undefined
124
- item: TItem
127
+ value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>
128
+ item: TTypeInfo['item']
125
129
  listKey: string
126
- fieldName: string
130
+ fieldName: TFieldKey
127
131
  context: import('../access/types.js').AccessContext
128
- }) => TOutput | undefined
132
+ }) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
129
133
  }
130
134
 
131
135
  /**
132
- * Configuration for patching Prisma-generated types
133
- * Allows fields to transform their types in query results
136
+ * Configuration for Prisma result extensions
137
+ * Allows fields to transform their runtime values and types in query results
138
+ *
139
+ * Runtime transformation is delegated to the field's resolveOutput hook.
140
+ * This config only specifies the TypeScript output type for generated types.
134
141
  */
135
- export type TypePatchConfig = {
142
+ export type ResultExtensionConfig = {
136
143
  /**
137
- * The TypeScript type to use in Prisma result types (e.g., Payload scalars)
138
- * This is an import statement like: "import('@opensaas/stack-core').HashedPassword"
144
+ * The TypeScript type to use in query result types
145
+ * This is a type expression like: "import('@opensaas/stack-core').HashedPassword"
146
+ *
147
+ * The actual runtime transformation is performed by the field's resolveOutput hook.
148
+ * The Prisma extension will automatically call the hook if it exists.
149
+ *
150
+ * @example "import('@opensaas/stack-core').HashedPassword"
151
+ * @example "import('./types').MyCustomType"
139
152
  */
140
- resultType: string
153
+ outputType: string
141
154
  /**
142
- * Optional: Where to apply the patch
143
- * - 'scalars-only': Only patch in Payload scalars (default, safest)
144
- * - 'all': Patch everywhere the field appears (including inputs)
155
+ * @deprecated No longer used. Runtime transformations are handled by resolveOutput hooks.
156
+ * This field is kept for backwards compatibility but should not be used in new code.
145
157
  */
146
- patchScope?: 'scalars-only' | 'all'
158
+ compute?: string
147
159
  }
148
160
 
149
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
- export type BaseFieldConfig<TInput = any, TOutput = TInput> = {
161
+ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
151
162
  type: string
152
163
  access?: FieldAccess
153
164
  defaultValue?: unknown
154
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
- hooks?: FieldHooks<TInput, TOutput, any>
165
+ hooks?: FieldHooks<TTypeInfo>
156
166
  /**
157
- * Type patching configuration for Prisma-generated types
158
- * When specified, the generator will patch Prisma's types to use
159
- * the specified type in query results instead of the original type
167
+ * Marks this field as virtual - not stored in database
168
+ * Virtual fields use resolveInput/resolveOutput hooks for computation
169
+ * They are excluded from Prisma schema and input types
170
+ * Only computed when explicitly selected/included in queries
160
171
  */
161
- typePatch?: TypePatchConfig
172
+ virtual?: boolean
173
+ /**
174
+ * Prisma result extension configuration
175
+ * Transforms field values and types in query results using Prisma's native extension system
176
+ */
177
+ resultExtension?: ResultExtensionConfig
162
178
  ui?: {
163
179
  /**
164
180
  * Custom React component to render this field
@@ -235,7 +251,7 @@ export type BaseFieldConfig<TInput = any, TOutput = TInput> = {
235
251
  }>
236
252
  }
237
253
 
238
- export type TextField = BaseFieldConfig<string, string> & {
254
+ export type TextField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
239
255
  type: 'text'
240
256
  validation?: {
241
257
  isRequired?: boolean
@@ -250,7 +266,7 @@ export type TextField = BaseFieldConfig<string, string> & {
250
266
  }
251
267
  }
252
268
 
253
- export type IntegerField = BaseFieldConfig<number, number> & {
269
+ export type IntegerField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
254
270
  type: 'integer'
255
271
  validation?: {
256
272
  isRequired?: boolean
@@ -259,26 +275,23 @@ export type IntegerField = BaseFieldConfig<number, number> & {
259
275
  }
260
276
  }
261
277
 
262
- export type CheckboxField = BaseFieldConfig<boolean, boolean> & {
278
+ export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
263
279
  type: 'checkbox'
264
280
  }
265
281
 
266
- export type TimestampField = BaseFieldConfig<Date, Date> & {
282
+ export type TimestampField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
267
283
  type: 'timestamp'
268
284
  defaultValue?: { kind: 'now' } | Date
269
285
  }
270
286
 
271
- export type PasswordField = BaseFieldConfig<
272
- string,
273
- import('../utils/password.js').HashedPassword
274
- > & {
287
+ export type PasswordField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
275
288
  type: 'password'
276
289
  validation?: {
277
290
  isRequired?: boolean
278
291
  }
279
292
  }
280
293
 
281
- export type SelectField = BaseFieldConfig<string, string> & {
294
+ export type SelectField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
282
295
  type: 'select'
283
296
  options: Array<{ label: string; value: string }>
284
297
  validation?: {
@@ -289,16 +302,17 @@ export type SelectField = BaseFieldConfig<string, string> & {
289
302
  }
290
303
  }
291
304
 
292
- export type RelationshipField = BaseFieldConfig<string | string[], string | string[]> & {
293
- type: 'relationship'
294
- ref: string // Format: 'ListName.fieldName'
295
- many?: boolean
296
- ui?: {
297
- displayMode?: 'select' | 'cards'
305
+ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
306
+ BaseFieldConfig<TTypeInfo> & {
307
+ type: 'relationship'
308
+ ref: string // Format: 'ListName.fieldName'
309
+ many?: boolean
310
+ ui?: {
311
+ displayMode?: 'select' | 'cards'
312
+ }
298
313
  }
299
- }
300
314
 
301
- export type JsonField = BaseFieldConfig<unknown, unknown> & {
315
+ export type JsonField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
302
316
  type: 'json'
303
317
  validation?: {
304
318
  isRequired?: boolean
@@ -310,39 +324,158 @@ export type JsonField = BaseFieldConfig<unknown, unknown> & {
310
324
  }
311
325
  }
312
326
 
313
- export type FieldConfig =
314
- | TextField
315
- | IntegerField
316
- | CheckboxField
317
- | TimestampField
318
- | PasswordField
319
- | SelectField
320
- | RelationshipField
321
- | JsonField
322
- | BaseFieldConfig // Allow any field extending BaseFieldConfig (for third-party fields)
327
+ export type VirtualField<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo> & {
328
+ type: 'virtual'
329
+ virtual: true
330
+ /**
331
+ * TypeScript type string for the virtual field output
332
+ * e.g., 'string', 'number', 'boolean', 'string[]', etc.
333
+ */
334
+ outputType: string
335
+ }
336
+
337
+ /**
338
+ * Generic field configuration type
339
+ * Simplified to just BaseFieldConfig to reduce type complexity
340
+ * Specific field types (TextField, IntegerField, etc.) are used by field builders
341
+ * but at the config level we treat all fields uniformly
342
+ */
343
+ export type FieldConfig = BaseFieldConfig<TypeInfo>
323
344
 
324
345
  /**
325
346
  * List configuration types
326
347
  */
327
348
 
328
349
  /**
329
- * Utility type to inject item type into a single field config
330
- * Extracts TInput and TOutput from BaseFieldConfig<TInput, TOutput> and reconstructs with new hooks type
350
+ * Utility type to inject TypeInfo into a single field config
351
+ * Extracts TInput and TOutput from BaseFieldConfig and reconstructs with new TypeInfo
331
352
  */
332
- type WithItemType<TField extends FieldConfig, TItem> =
333
- TField extends BaseFieldConfig<infer TInput, infer TOutput>
334
- ? Omit<TField, 'hooks'> & {
335
- hooks?: FieldHooks<TInput, TOutput, TItem>
336
- }
337
- : TField
353
+ type WithTypeInfo<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo>
338
354
 
339
355
  /**
340
- * Utility type to transform all fields in a record to inject item type
341
- * Maps over each field and applies WithItemType transformation
356
+ * Utility type to transform all fields in a record to inject TypeInfo
357
+ * Maps over each field and applies WithTypeInfo transformation
342
358
  */
343
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
344
- export type FieldsWithItemType<TFields extends Record<string, FieldConfig>, TItem = any> = {
345
- [K in keyof TFields]: WithItemType<TFields[K], TItem>
359
+ export type FieldsWithTypeInfo<TTypeInfo extends TypeInfo> = {
360
+ [key: string]: WithTypeInfo<TTypeInfo>
361
+ }
362
+
363
+ /**
364
+ * Parse TypeScript type string to actual type
365
+ * Handles: 'string', 'number', 'boolean', 'Date', unions, string literals, imports
366
+ *
367
+ * @example
368
+ * ParseTypeString<'string'> => string
369
+ * ParseTypeString<'number'> => number
370
+ * ParseTypeString<"'draft' | 'published'"> => 'draft' | 'published'
371
+ * ParseTypeString<"import('@opensaas/stack-core').HashedPassword"> => any (fallback for imports)
372
+ */
373
+ type ParseTypeString<T extends string> = T extends 'string'
374
+ ? string
375
+ : T extends 'number'
376
+ ? number
377
+ : T extends 'boolean'
378
+ ? boolean
379
+ : T extends 'Date'
380
+ ? Date
381
+ : T extends 'unknown'
382
+ ? unknown
383
+ : T extends `'${infer U}'`
384
+ ? U // String literal
385
+ : T extends `${infer U} | ${infer V}`
386
+ ? ParseTypeString<U> | ParseTypeString<V> // Union
387
+ : T extends `import(${string}).${string}`
388
+ ? any // eslint-disable-line @typescript-eslint/no-explicit-any -- Import types can't be resolved at compile time
389
+ : unknown // Fallback
390
+
391
+ /**
392
+ * Extract field value type from a field config
393
+ * Uses the field's getTypeScriptType() method result
394
+ * If resultExtension is present, uses its outputType instead
395
+ *
396
+ * @example
397
+ * ExtractFieldValueType<TextField> => string | null | undefined (if optional)
398
+ * ExtractFieldValueType<IntegerField> => number
399
+ * ExtractFieldValueType<PasswordField> => HashedPassword (from resultExtension)
400
+ */
401
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility type needs to accept any BaseFieldConfig
402
+ type ExtractFieldValueType<TField extends BaseFieldConfig<any>> = TField extends {
403
+ resultExtension: { outputType: infer O }
404
+ }
405
+ ? ParseTypeString<O & string>
406
+ : TField extends { getTypeScriptType(): { type: infer T; optional: infer Opt } }
407
+ ? Opt extends true
408
+ ? ParseTypeString<T & string> | null | undefined
409
+ : ParseTypeString<T & string>
410
+ : unknown
411
+
412
+ /**
413
+ * Extract field names as union of string literals
414
+ *
415
+ * @example
416
+ * FieldKeys<{ title: TextField, content: TextField }> => 'title' | 'content'
417
+ */
418
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility type needs to accept any field record
419
+ export type FieldKeys<TFields extends Record<string, any>> = keyof TFields & string
420
+
421
+ /**
422
+ * Get field config for a specific field name
423
+ * Preserves the specific field type (TextField, PasswordField, etc.)
424
+ *
425
+ * @example
426
+ * GetFieldConfig<{ title: TextField }, 'title'> => TextField
427
+ */
428
+ export type GetFieldConfig<
429
+ TFields extends Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- Generic utility type needs to accept any field record
430
+ TFieldKey extends FieldKeys<TFields>,
431
+ > = TFields[TFieldKey]
432
+
433
+ /**
434
+ * Get value type for a specific field
435
+ *
436
+ * @example
437
+ * GetFieldValueType<{ title: TextField }, 'title'> => string
438
+ */
439
+ export type GetFieldValueType<
440
+ TFields extends Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- Generic utility type needs to accept any field record
441
+ TFieldKey extends FieldKeys<TFields>,
442
+ > = ExtractFieldValueType<GetFieldConfig<TFields, TFieldKey>>
443
+
444
+ /**
445
+ * TypeInfo interface for list type information
446
+ * Provides a structured way to pass all type information for a list
447
+ * Inspired by Keystone's TypeInfo pattern
448
+ *
449
+ * @template TKey - The list key/name (e.g., 'Post', 'User')
450
+ * @template TFields - The fields configuration for the list
451
+ * @template TItem - The output type (Prisma model type)
452
+ * @template TCreateInput - The Prisma create input type
453
+ * @template TUpdateInput - The Prisma update input type
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * type PostTypeInfo = {
458
+ * key: 'Post'
459
+ * fields: { title: TextField<...>, content: TextField<...> }
460
+ * item: Post
461
+ * inputs: {
462
+ * create: Prisma.PostCreateInput
463
+ * update: Prisma.PostUpdateInput
464
+ * }
465
+ * }
466
+ * ```
467
+ */
468
+ export interface TypeInfo<
469
+ TKey extends string = string,
470
+ TFields extends Record<string, any> = Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- TypeInfo must accept any field record
471
+ > {
472
+ key: TKey
473
+ fields: TFields
474
+ item: any // eslint-disable-line @typescript-eslint/no-explicit-any -- Item type is provided by Prisma and varies per list
475
+ inputs: {
476
+ create: any // eslint-disable-line @typescript-eslint/no-explicit-any -- Prisma input types are generated and vary per list
477
+ update: any // eslint-disable-line @typescript-eslint/no-explicit-any -- Prisma input types are generated and vary per list
478
+ }
346
479
  }
347
480
 
348
481
  // Generic `any` default allows OperationAccess to work with any list item type
@@ -355,36 +488,73 @@ export type OperationAccess<T = any> = {
355
488
  delete?: AccessControl<T>
356
489
  }
357
490
 
358
- export type HookArgs<T = Record<string, unknown>> = {
491
+ /**
492
+ * Hook arguments for resolveInput hook
493
+ * Uses discriminated union to provide proper types based on operation
494
+ * - create: resolvedData is CreateInput, item is undefined
495
+ * - update: resolvedData is UpdateInput, item is the existing record
496
+ */
497
+ export type ResolveInputHookArgs<
498
+ TOutput = Record<string, unknown>,
499
+ TCreateInput = Record<string, unknown>,
500
+ TUpdateInput = Record<string, unknown>,
501
+ > =
502
+ | {
503
+ operation: 'create'
504
+ resolvedData: TCreateInput
505
+ item: undefined
506
+ context: import('../access/types.js').AccessContext
507
+ }
508
+ | {
509
+ operation: 'update'
510
+ resolvedData: TUpdateInput
511
+ item: TOutput
512
+ context: import('../access/types.js').AccessContext
513
+ }
514
+
515
+ /**
516
+ * Hook arguments for other hooks (validateInput, beforeOperation, afterOperation)
517
+ * These hooks receive the same structure regardless of operation
518
+ */
519
+ export type HookArgs<
520
+ TOutput = Record<string, unknown>,
521
+ TCreateInput = Record<string, unknown>,
522
+ TUpdateInput = Record<string, unknown>,
523
+ > = {
359
524
  operation: 'create' | 'update' | 'delete'
360
- resolvedData?: Partial<T>
361
- item?: T
525
+ resolvedData?: TCreateInput | TUpdateInput
526
+ item?: TOutput
362
527
  context: import('../access/types.js').AccessContext
363
528
  }
364
529
 
365
- export type Hooks<T = Record<string, unknown>> = {
366
- resolveInput?: (args: HookArgs<T> & { operation: 'create' | 'update' }) => Promise<Partial<T>>
530
+ export type Hooks<
531
+ TOutput = Record<string, unknown>,
532
+ TCreateInput = Record<string, unknown>,
533
+ TUpdateInput = Record<string, unknown>,
534
+ > = {
535
+ resolveInput?: (
536
+ args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
537
+ ) => Promise<TCreateInput | TUpdateInput>
367
538
  validateInput?: (
368
- args: HookArgs<T> & {
539
+ args: HookArgs<TOutput, TCreateInput, TUpdateInput> & {
369
540
  operation: 'create' | 'update'
370
541
  addValidationError: (msg: string) => void
371
542
  },
372
543
  ) => Promise<void>
373
- beforeOperation?: (args: HookArgs<T>) => Promise<void>
374
- afterOperation?: (args: HookArgs<T>) => Promise<void>
544
+ beforeOperation?: (args: HookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
545
+ afterOperation?: (args: HookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
375
546
  }
376
547
 
377
548
  // Generic `any` default allows ListConfig to work with any list item type
378
549
  // This is needed because the item type varies per list and is inferred from Prisma models
379
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
- export type ListConfig<T = any> = {
381
- // Field configs are automatically transformed to inject the item type T
382
- // This enables proper typing in field hooks where item: TItem
383
- fields: FieldsWithItemType<Record<string, FieldConfig>, T>
550
+ export type ListConfig<TTypeInfo extends TypeInfo> = {
551
+ // Field configs are automatically transformed to inject the full TypeInfo
552
+ // This enables proper typing in field hooks where item, create input, and update input are all typed
553
+ fields: FieldsWithTypeInfo<TTypeInfo>
384
554
  access?: {
385
- operation?: OperationAccess<T>
555
+ operation?: OperationAccess<TTypeInfo['item']>
386
556
  }
387
- hooks?: Hooks<T>
557
+ hooks?: Hooks<TTypeInfo['item'], TTypeInfo['inputs']['create'], TTypeInfo['inputs']['update']>
388
558
  /**
389
559
  * MCP server configuration for this list
390
560
  */
@@ -396,12 +566,37 @@ export type ListConfig<T = any> = {
396
566
  */
397
567
  export type DatabaseConfig = {
398
568
  provider: 'postgresql' | 'mysql' | 'sqlite'
399
- url: string
400
569
  /**
401
- * Optional factory function to create a custom Prisma client instance
402
- * Receives the PrismaClient class and returns a configured instance
570
+ * Factory function to create a Prisma client instance with a database adapter
571
+ * Required in Prisma 7+ - receives the PrismaClient class and returns a configured instance
403
572
  *
404
- * @example
573
+ * The connection URL is passed directly to the adapter, not to the config.
574
+ *
575
+ * @example SQLite with better-sqlite3
576
+ * ```typescript
577
+ * import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'
578
+ * import Database from 'better-sqlite3'
579
+ *
580
+ * prismaClientConstructor: (PrismaClient) => {
581
+ * const db = new Database(process.env.DATABASE_URL || './dev.db')
582
+ * const adapter = new PrismaBetterSQLite3(db)
583
+ * return new PrismaClient({ adapter })
584
+ * }
585
+ * ```
586
+ *
587
+ * @example PostgreSQL with pg
588
+ * ```typescript
589
+ * import { PrismaPg } from '@prisma/adapter-pg'
590
+ * import pg from 'pg'
591
+ *
592
+ * prismaClientConstructor: (PrismaClient) => {
593
+ * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
594
+ * const adapter = new PrismaPg(pool)
595
+ * return new PrismaClient({ adapter })
596
+ * }
597
+ * ```
598
+ *
599
+ * @example Neon serverless (PostgreSQL)
405
600
  * ```typescript
406
601
  * import { PrismaNeon } from '@prisma/adapter-neon'
407
602
  * import { neonConfig } from '@neondatabase/serverless'
@@ -419,7 +614,7 @@ export type DatabaseConfig = {
419
614
  // Uses `any` for maximum flexibility with Prisma client constructors and adapters
420
615
  // Different database adapters have varying type signatures that are hard to unify
421
616
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
422
- prismaClientConstructor?: (PrismaClientClass: any) => any
617
+ prismaClientConstructor: (PrismaClientClass: any) => any
423
618
  }
424
619
 
425
620
  /**
@@ -763,7 +958,8 @@ export type PluginContext = {
763
958
  * Add a new list to the config
764
959
  * Throws error if list already exists (unless merge strategy used)
765
960
  */
766
- addList: (name: string, listConfig: ListConfig) => void
961
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API must accept any list config
962
+ addList: (name: string, listConfig: ListConfig<any>) => void
767
963
 
768
964
  /**
769
965
  * Extend an existing list with additional fields, hooks, or access control
@@ -786,7 +982,8 @@ export type PluginContext = {
786
982
  * Register a field type globally
787
983
  * Useful for third-party field packages
788
984
  */
789
- registerFieldType?: (type: string, builder: (options?: unknown) => BaseFieldConfig) => void
985
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API must accept any field config builder
986
+ registerFieldType?: (type: string, builder: (options?: unknown) => BaseFieldConfig<any>) => void
790
987
 
791
988
  /**
792
989
  * Register a custom MCP tool
@@ -846,14 +1043,41 @@ export type Plugin = {
846
1043
  * Return value is stored in context.plugins[pluginName]
847
1044
  */
848
1045
  runtime?: (context: import('../access/types.js').AccessContext) => unknown
1046
+
1047
+ /**
1048
+ * Optional: Type metadata for runtime services
1049
+ * Enables type-safe code generation for context.plugins
1050
+ *
1051
+ * @example
1052
+ * ```typescript
1053
+ * {
1054
+ * import: "import type { AuthRuntimeServices } from '@opensaas/stack-auth/runtime'",
1055
+ * typeName: "AuthRuntimeServices"
1056
+ * }
1057
+ * ```
1058
+ */
1059
+ runtimeServiceTypes?: {
1060
+ /**
1061
+ * Import statement to include in generated types file
1062
+ * Must be a complete import statement with 'import type' and quotes
1063
+ */
1064
+ import: string
1065
+ /**
1066
+ * TypeScript type name to use in PluginServices interface
1067
+ * Should match the exported type from the import
1068
+ */
1069
+ typeName: string
1070
+ }
849
1071
  }
850
1072
 
851
1073
  /**
852
1074
  * Main configuration type
1075
+ * Using interface instead of type to allow module augmentation
853
1076
  */
854
- export type OpenSaasConfig = {
1077
+ export interface OpenSaasConfig {
855
1078
  db: DatabaseConfig
856
- lists: Record<string, ListConfig>
1079
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Config must accept any list configuration
1080
+ lists: Record<string, ListConfig<any>>
857
1081
  session?: SessionConfig
858
1082
  ui?: UIConfig
859
1083
  /**
@@ -881,4 +1105,10 @@ export type OpenSaasConfig = {
881
1105
  * @internal
882
1106
  */
883
1107
  _pluginData?: Record<string, unknown>
1108
+ /**
1109
+ * Sorted plugin instances (stored after plugin execution)
1110
+ * Used at runtime to call plugin.runtime() functions
1111
+ * @internal
1112
+ */
1113
+ _plugins?: Plugin[]
884
1114
  }