@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +352 -0
- package/CLAUDE.md +46 -1
- package/dist/access/engine.d.ts +7 -6
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +55 -0
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.d.ts +2 -0
- package/dist/access/engine.test.d.ts.map +1 -0
- package/dist/access/engine.test.js +125 -0
- package/dist/access/engine.test.js.map +1 -0
- package/dist/access/types.d.ts +39 -9
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +40 -20
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +34 -15
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +9 -0
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +277 -84
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +5 -3
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +146 -20
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +88 -72
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/fields/index.d.ts +65 -9
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +98 -16
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts +28 -12
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +16 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/handler.js +1 -0
- package/dist/mcp/handler.js.map +1 -1
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +4 -2
- package/dist/validation/schema.js.map +1 -1
- package/package.json +8 -9
- package/src/access/engine.test.ts +145 -0
- package/src/access/engine.ts +73 -9
- package/src/access/types.ts +38 -8
- package/src/config/index.ts +45 -23
- package/src/config/plugin-engine.ts +13 -3
- package/src/config/types.ts +347 -117
- package/src/context/index.ts +176 -23
- package/src/context/nested-operations.ts +83 -71
- package/src/fields/index.ts +132 -27
- package/src/hooks/index.ts +63 -20
- package/src/index.ts +9 -0
- package/src/mcp/handler.ts +2 -1
- package/src/validation/schema.ts +4 -2
- package/tests/context.test.ts +38 -6
- package/tests/field-types.test.ts +729 -0
- package/tests/password-type-distribution.test.ts +0 -1
- package/tests/password-types.test.ts +0 -1
- package/tests/plugin-engine.test.ts +1102 -0
- package/tests/sudo.test.ts +230 -2
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/types.ts
CHANGED
|
@@ -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
|
|
22
|
-
* @template
|
|
23
|
-
*
|
|
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
|
-
|
|
26
|
-
|
|
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:
|
|
45
|
-
item?:
|
|
56
|
+
inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
57
|
+
item?: TTypeInfo['item']
|
|
46
58
|
listKey: string
|
|
47
|
-
fieldName:
|
|
59
|
+
fieldName: TFieldKey
|
|
48
60
|
context: import('../access/types.js').AccessContext
|
|
49
|
-
}) =>
|
|
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:
|
|
67
|
-
item?:
|
|
81
|
+
resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
82
|
+
item?: TTypeInfo['item']
|
|
68
83
|
listKey: string
|
|
69
|
-
fieldName:
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
124
|
-
item:
|
|
127
|
+
value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>
|
|
128
|
+
item: TTypeInfo['item']
|
|
125
129
|
listKey: string
|
|
126
|
-
fieldName:
|
|
130
|
+
fieldName: TFieldKey
|
|
127
131
|
context: import('../access/types.js').AccessContext
|
|
128
|
-
}) =>
|
|
132
|
+
}) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/**
|
|
132
|
-
* Configuration for
|
|
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
|
|
142
|
+
export type ResultExtensionConfig = {
|
|
136
143
|
/**
|
|
137
|
-
* The TypeScript type to use in
|
|
138
|
-
* This is
|
|
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
|
-
|
|
153
|
+
outputType: string
|
|
141
154
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
-
|
|
158
|
+
compute?: string
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
hooks?: FieldHooks<TInput, TOutput, any>
|
|
165
|
+
hooks?: FieldHooks<TTypeInfo>
|
|
156
166
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
278
|
+
export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
263
279
|
type: 'checkbox'
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
export type TimestampField = BaseFieldConfig<
|
|
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<
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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<
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
330
|
-
* Extracts TInput and TOutput from BaseFieldConfig
|
|
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
|
|
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
|
|
341
|
-
* Maps over each field and applies
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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?:
|
|
361
|
-
item?:
|
|
525
|
+
resolvedData?: TCreateInput | TUpdateInput
|
|
526
|
+
item?: TOutput
|
|
362
527
|
context: import('../access/types.js').AccessContext
|
|
363
528
|
}
|
|
364
529
|
|
|
365
|
-
export type Hooks<
|
|
366
|
-
|
|
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<
|
|
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<
|
|
374
|
-
afterOperation?: (args: HookArgs<
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
//
|
|
382
|
-
|
|
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<
|
|
555
|
+
operation?: OperationAccess<TTypeInfo['item']>
|
|
386
556
|
}
|
|
387
|
-
hooks?: Hooks<
|
|
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
|
-
*
|
|
402
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1077
|
+
export interface OpenSaasConfig {
|
|
855
1078
|
db: DatabaseConfig
|
|
856
|
-
|
|
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
|
}
|