@opensaas/stack-core 0.3.0 → 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 (47) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +150 -0
  3. package/dist/access/engine.d.ts +1 -1
  4. package/dist/access/engine.d.ts.map +1 -1
  5. package/dist/access/engine.js +38 -0
  6. package/dist/access/engine.js.map +1 -1
  7. package/dist/config/index.d.ts +5 -5
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +0 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/plugin-engine.d.ts.map +1 -1
  12. package/dist/config/plugin-engine.js +3 -0
  13. package/dist/config/plugin-engine.js.map +1 -1
  14. package/dist/config/types.d.ts +159 -73
  15. package/dist/config/types.d.ts.map +1 -1
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js +19 -6
  18. package/dist/context/index.js.map +1 -1
  19. package/dist/context/nested-operations.d.ts.map +1 -1
  20. package/dist/context/nested-operations.js +88 -72
  21. package/dist/context/nested-operations.js.map +1 -1
  22. package/dist/fields/index.d.ts +65 -9
  23. package/dist/fields/index.d.ts.map +1 -1
  24. package/dist/fields/index.js +89 -8
  25. package/dist/fields/index.js.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp/handler.js +1 -0
  30. package/dist/mcp/handler.js.map +1 -1
  31. package/dist/validation/schema.d.ts.map +1 -1
  32. package/dist/validation/schema.js +4 -2
  33. package/dist/validation/schema.js.map +1 -1
  34. package/package.json +7 -7
  35. package/src/access/engine.ts +48 -3
  36. package/src/config/index.ts +8 -13
  37. package/src/config/plugin-engine.ts +6 -3
  38. package/src/config/types.ts +208 -109
  39. package/src/context/index.ts +14 -7
  40. package/src/context/nested-operations.ts +83 -71
  41. package/src/fields/index.ts +124 -20
  42. package/src/index.ts +9 -0
  43. package/src/mcp/handler.ts +2 -1
  44. package/src/validation/schema.ts +4 -2
  45. package/tests/field-types.test.ts +6 -5
  46. package/tests/sudo.test.ts +230 -1
  47. 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>
166
+ /**
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
171
+ */
172
+ virtual?: boolean
156
173
  /**
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
174
+ * Prisma result extension configuration
175
+ * Transforms field values and types in query results using Prisma's native extension system
160
176
  */
161
- typePatch?: TypePatchConfig
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,47 +324,130 @@ 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>
346
361
  }
347
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
+
348
444
  /**
349
445
  * TypeInfo interface for list type information
350
446
  * Provides a structured way to pass all type information for a list
351
447
  * Inspired by Keystone's TypeInfo pattern
352
448
  *
353
449
  * @template TKey - The list key/name (e.g., 'Post', 'User')
450
+ * @template TFields - The fields configuration for the list
354
451
  * @template TItem - The output type (Prisma model type)
355
452
  * @template TCreateInput - The Prisma create input type
356
453
  * @template TUpdateInput - The Prisma update input type
@@ -359,6 +456,7 @@ export type FieldsWithItemType<TFields extends Record<string, FieldConfig>, TIte
359
456
  * ```typescript
360
457
  * type PostTypeInfo = {
361
458
  * key: 'Post'
459
+ * fields: { title: TextField<...>, content: TextField<...> }
362
460
  * item: Post
363
461
  * inputs: {
364
462
  * create: Prisma.PostCreateInput
@@ -369,15 +467,14 @@ export type FieldsWithItemType<TFields extends Record<string, FieldConfig>, TIte
369
467
  */
370
468
  export interface TypeInfo<
371
469
  TKey extends string = string,
372
- TItem = any, // eslint-disable-line @typescript-eslint/no-explicit-any
373
- TCreateInput = any, // eslint-disable-line @typescript-eslint/no-explicit-any
374
- TUpdateInput = any, // eslint-disable-line @typescript-eslint/no-explicit-any
470
+ TFields extends Record<string, any> = Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any -- TypeInfo must accept any field record
375
471
  > {
376
472
  key: TKey
377
- item: TItem
473
+ fields: TFields
474
+ item: any // eslint-disable-line @typescript-eslint/no-explicit-any -- Item type is provided by Prisma and varies per list
378
475
  inputs: {
379
- create: TCreateInput
380
- update: TUpdateInput
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
381
478
  }
382
479
  }
383
480
 
@@ -450,15 +547,14 @@ export type Hooks<
450
547
 
451
548
  // Generic `any` default allows ListConfig to work with any list item type
452
549
  // This is needed because the item type varies per list and is inferred from Prisma models
453
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
454
- export type ListConfig<TOutput = any, TCreateInput = any, TUpdateInput = any> = {
455
- // Field configs are automatically transformed to inject the item type T
456
- // This enables proper typing in field hooks where item: TItem
457
- fields: FieldsWithItemType<Record<string, FieldConfig>, TOutput>
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>
458
554
  access?: {
459
- operation?: OperationAccess<TOutput>
555
+ operation?: OperationAccess<TTypeInfo['item']>
460
556
  }
461
- hooks?: Hooks<TOutput, TCreateInput, TUpdateInput>
557
+ hooks?: Hooks<TTypeInfo['item'], TTypeInfo['inputs']['create'], TTypeInfo['inputs']['update']>
462
558
  /**
463
559
  * MCP server configuration for this list
464
560
  */
@@ -862,7 +958,8 @@ export type PluginContext = {
862
958
  * Add a new list to the config
863
959
  * Throws error if list already exists (unless merge strategy used)
864
960
  */
865
- 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
866
963
 
867
964
  /**
868
965
  * Extend an existing list with additional fields, hooks, or access control
@@ -885,7 +982,8 @@ export type PluginContext = {
885
982
  * Register a field type globally
886
983
  * Useful for third-party field packages
887
984
  */
888
- 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
889
987
 
890
988
  /**
891
989
  * Register a custom MCP tool
@@ -978,7 +1076,8 @@ export type Plugin = {
978
1076
  */
979
1077
  export interface OpenSaasConfig {
980
1078
  db: DatabaseConfig
981
- 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>>
982
1081
  session?: SessionConfig
983
1082
  ui?: UIConfig
984
1083
  /**
@@ -135,7 +135,8 @@ export type ServerActionProps =
135
135
  /**
136
136
  * Parse Prisma error and convert to user-friendly DatabaseError
137
137
  */
138
- function parsePrismaError(error: unknown, listConfig: ListConfig): Error {
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
139
+ function parsePrismaError(error: unknown, listConfig: ListConfig<any>): Error {
139
140
  // Check if it's a Prisma error
140
141
  if (
141
142
  error &&
@@ -406,7 +407,8 @@ export function getContext<
406
407
  */
407
408
  function createFindUnique<TPrisma extends PrismaClientLike>(
408
409
  listName: string,
409
- listConfig: ListConfig,
410
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
411
+ listConfig: ListConfig<any>,
410
412
  prisma: TPrisma,
411
413
  context: AccessContext<TPrisma>,
412
414
  config: OpenSaasConfig,
@@ -492,7 +494,8 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
492
494
  */
493
495
  function createFindMany<TPrisma extends PrismaClientLike>(
494
496
  listName: string,
495
- listConfig: ListConfig,
497
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
498
+ listConfig: ListConfig<any>,
496
499
  prisma: TPrisma,
497
500
  context: AccessContext<TPrisma>,
498
501
  config: OpenSaasConfig,
@@ -589,7 +592,8 @@ function createFindMany<TPrisma extends PrismaClientLike>(
589
592
  */
590
593
  function createCreate<TPrisma extends PrismaClientLike>(
591
594
  listName: string,
592
- listConfig: ListConfig,
595
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
596
+ listConfig: ListConfig<any>,
593
597
  prisma: TPrisma,
594
598
  context: AccessContext<TPrisma>,
595
599
  config: OpenSaasConfig,
@@ -709,7 +713,8 @@ function createCreate<TPrisma extends PrismaClientLike>(
709
713
  */
710
714
  function createUpdate<TPrisma extends PrismaClientLike>(
711
715
  listName: string,
712
- listConfig: ListConfig,
716
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
717
+ listConfig: ListConfig<any>,
713
718
  prisma: TPrisma,
714
719
  context: AccessContext<TPrisma>,
715
720
  config: OpenSaasConfig,
@@ -863,7 +868,8 @@ function createUpdate<TPrisma extends PrismaClientLike>(
863
868
  */
864
869
  function createDelete<TPrisma extends PrismaClientLike>(
865
870
  listName: string,
866
- listConfig: ListConfig,
871
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
872
+ listConfig: ListConfig<any>,
867
873
  prisma: TPrisma,
868
874
  context: AccessContext<TPrisma>,
869
875
  ) {
@@ -946,7 +952,8 @@ function createDelete<TPrisma extends PrismaClientLike>(
946
952
  */
947
953
  function createCount<TPrisma extends PrismaClientLike>(
948
954
  listName: string,
949
- listConfig: ListConfig,
955
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
956
+ listConfig: ListConfig<any>,
950
957
  prisma: TPrisma,
951
958
  context: AccessContext<TPrisma>,
952
959
  ) {