@opensaas/stack-core 0.1.6 → 0.3.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 (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +46 -1
  4. package/dist/access/engine.d.ts +15 -8
  5. package/dist/access/engine.d.ts.map +1 -1
  6. package/dist/access/engine.js +23 -2
  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 +40 -9
  13. package/dist/access/types.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +38 -18
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +34 -14
  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 +6 -0
  20. package/dist/config/plugin-engine.js.map +1 -1
  21. package/dist/config/types.d.ts +128 -21
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/context/index.d.ts +14 -2
  24. package/dist/context/index.d.ts.map +1 -1
  25. package/dist/context/index.js +243 -100
  26. package/dist/context/index.js.map +1 -1
  27. package/dist/fields/index.d.ts.map +1 -1
  28. package/dist/fields/index.js +9 -8
  29. package/dist/fields/index.js.map +1 -1
  30. package/dist/hooks/index.d.ts +28 -12
  31. package/dist/hooks/index.d.ts.map +1 -1
  32. package/dist/hooks/index.js +16 -0
  33. package/dist/hooks/index.js.map +1 -1
  34. package/package.json +3 -4
  35. package/src/access/engine.test.ts +145 -0
  36. package/src/access/engine.ts +35 -11
  37. package/src/access/types.ts +39 -8
  38. package/src/config/index.ts +46 -19
  39. package/src/config/plugin-engine.ts +7 -0
  40. package/src/config/types.ts +149 -18
  41. package/src/context/index.ts +298 -110
  42. package/src/fields/index.ts +8 -7
  43. package/src/hooks/index.ts +63 -20
  44. package/tests/context.test.ts +38 -6
  45. package/tests/field-types.test.ts +728 -0
  46. package/tests/password-type-distribution.test.ts +0 -1
  47. package/tests/password-types.test.ts +0 -1
  48. package/tests/plugin-engine.test.ts +1102 -0
  49. package/tests/sudo.test.ts +405 -0
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -345,6 +345,42 @@ export type FieldsWithItemType<TFields extends Record<string, FieldConfig>, TIte
345
345
  [K in keyof TFields]: WithItemType<TFields[K], TItem>
346
346
  }
347
347
 
348
+ /**
349
+ * TypeInfo interface for list type information
350
+ * Provides a structured way to pass all type information for a list
351
+ * Inspired by Keystone's TypeInfo pattern
352
+ *
353
+ * @template TKey - The list key/name (e.g., 'Post', 'User')
354
+ * @template TItem - The output type (Prisma model type)
355
+ * @template TCreateInput - The Prisma create input type
356
+ * @template TUpdateInput - The Prisma update input type
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * type PostTypeInfo = {
361
+ * key: 'Post'
362
+ * item: Post
363
+ * inputs: {
364
+ * create: Prisma.PostCreateInput
365
+ * update: Prisma.PostUpdateInput
366
+ * }
367
+ * }
368
+ * ```
369
+ */
370
+ export interface TypeInfo<
371
+ 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
375
+ > {
376
+ key: TKey
377
+ item: TItem
378
+ inputs: {
379
+ create: TCreateInput
380
+ update: TUpdateInput
381
+ }
382
+ }
383
+
348
384
  // Generic `any` default allows OperationAccess to work with any list item type
349
385
  // This is needed because the item type varies per list and is inferred from Prisma models
350
386
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -355,36 +391,74 @@ export type OperationAccess<T = any> = {
355
391
  delete?: AccessControl<T>
356
392
  }
357
393
 
358
- export type HookArgs<T = Record<string, unknown>> = {
394
+ /**
395
+ * Hook arguments for resolveInput hook
396
+ * Uses discriminated union to provide proper types based on operation
397
+ * - create: resolvedData is CreateInput, item is undefined
398
+ * - update: resolvedData is UpdateInput, item is the existing record
399
+ */
400
+ export type ResolveInputHookArgs<
401
+ TOutput = Record<string, unknown>,
402
+ TCreateInput = Record<string, unknown>,
403
+ TUpdateInput = Record<string, unknown>,
404
+ > =
405
+ | {
406
+ operation: 'create'
407
+ resolvedData: TCreateInput
408
+ item: undefined
409
+ context: import('../access/types.js').AccessContext
410
+ }
411
+ | {
412
+ operation: 'update'
413
+ resolvedData: TUpdateInput
414
+ item: TOutput
415
+ context: import('../access/types.js').AccessContext
416
+ }
417
+
418
+ /**
419
+ * Hook arguments for other hooks (validateInput, beforeOperation, afterOperation)
420
+ * These hooks receive the same structure regardless of operation
421
+ */
422
+ export type HookArgs<
423
+ TOutput = Record<string, unknown>,
424
+ TCreateInput = Record<string, unknown>,
425
+ TUpdateInput = Record<string, unknown>,
426
+ > = {
359
427
  operation: 'create' | 'update' | 'delete'
360
- resolvedData?: Partial<T>
361
- item?: T
428
+ resolvedData?: TCreateInput | TUpdateInput
429
+ item?: TOutput
362
430
  context: import('../access/types.js').AccessContext
363
431
  }
364
432
 
365
- export type Hooks<T = Record<string, unknown>> = {
366
- resolveInput?: (args: HookArgs<T> & { operation: 'create' | 'update' }) => Promise<Partial<T>>
433
+ export type Hooks<
434
+ TOutput = Record<string, unknown>,
435
+ TCreateInput = Record<string, unknown>,
436
+ TUpdateInput = Record<string, unknown>,
437
+ > = {
438
+ resolveInput?: (
439
+ args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
440
+ ) => Promise<TCreateInput | TUpdateInput>
367
441
  validateInput?: (
368
- args: HookArgs<T> & {
442
+ args: HookArgs<TOutput, TCreateInput, TUpdateInput> & {
369
443
  operation: 'create' | 'update'
370
444
  addValidationError: (msg: string) => void
371
445
  },
372
446
  ) => Promise<void>
373
- beforeOperation?: (args: HookArgs<T>) => Promise<void>
374
- afterOperation?: (args: HookArgs<T>) => Promise<void>
447
+ beforeOperation?: (args: HookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
448
+ afterOperation?: (args: HookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
375
449
  }
376
450
 
377
451
  // Generic `any` default allows ListConfig to work with any list item type
378
452
  // This is needed because the item type varies per list and is inferred from Prisma models
379
453
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
- export type ListConfig<T = any> = {
454
+ export type ListConfig<TOutput = any, TCreateInput = any, TUpdateInput = any> = {
381
455
  // Field configs are automatically transformed to inject the item type T
382
456
  // This enables proper typing in field hooks where item: TItem
383
- fields: FieldsWithItemType<Record<string, FieldConfig>, T>
457
+ fields: FieldsWithItemType<Record<string, FieldConfig>, TOutput>
384
458
  access?: {
385
- operation?: OperationAccess<T>
459
+ operation?: OperationAccess<TOutput>
386
460
  }
387
- hooks?: Hooks<T>
461
+ hooks?: Hooks<TOutput, TCreateInput, TUpdateInput>
388
462
  /**
389
463
  * MCP server configuration for this list
390
464
  */
@@ -396,12 +470,37 @@ export type ListConfig<T = any> = {
396
470
  */
397
471
  export type DatabaseConfig = {
398
472
  provider: 'postgresql' | 'mysql' | 'sqlite'
399
- url: string
400
473
  /**
401
- * Optional factory function to create a custom Prisma client instance
402
- * Receives the PrismaClient class and returns a configured instance
474
+ * Factory function to create a Prisma client instance with a database adapter
475
+ * Required in Prisma 7+ - receives the PrismaClient class and returns a configured instance
403
476
  *
404
- * @example
477
+ * The connection URL is passed directly to the adapter, not to the config.
478
+ *
479
+ * @example SQLite with better-sqlite3
480
+ * ```typescript
481
+ * import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'
482
+ * import Database from 'better-sqlite3'
483
+ *
484
+ * prismaClientConstructor: (PrismaClient) => {
485
+ * const db = new Database(process.env.DATABASE_URL || './dev.db')
486
+ * const adapter = new PrismaBetterSQLite3(db)
487
+ * return new PrismaClient({ adapter })
488
+ * }
489
+ * ```
490
+ *
491
+ * @example PostgreSQL with pg
492
+ * ```typescript
493
+ * import { PrismaPg } from '@prisma/adapter-pg'
494
+ * import pg from 'pg'
495
+ *
496
+ * prismaClientConstructor: (PrismaClient) => {
497
+ * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
498
+ * const adapter = new PrismaPg(pool)
499
+ * return new PrismaClient({ adapter })
500
+ * }
501
+ * ```
502
+ *
503
+ * @example Neon serverless (PostgreSQL)
405
504
  * ```typescript
406
505
  * import { PrismaNeon } from '@prisma/adapter-neon'
407
506
  * import { neonConfig } from '@neondatabase/serverless'
@@ -419,7 +518,7 @@ export type DatabaseConfig = {
419
518
  // Uses `any` for maximum flexibility with Prisma client constructors and adapters
420
519
  // Different database adapters have varying type signatures that are hard to unify
421
520
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
422
- prismaClientConstructor?: (PrismaClientClass: any) => any
521
+ prismaClientConstructor: (PrismaClientClass: any) => any
423
522
  }
424
523
 
425
524
  /**
@@ -846,12 +945,38 @@ export type Plugin = {
846
945
  * Return value is stored in context.plugins[pluginName]
847
946
  */
848
947
  runtime?: (context: import('../access/types.js').AccessContext) => unknown
948
+
949
+ /**
950
+ * Optional: Type metadata for runtime services
951
+ * Enables type-safe code generation for context.plugins
952
+ *
953
+ * @example
954
+ * ```typescript
955
+ * {
956
+ * import: "import type { AuthRuntimeServices } from '@opensaas/stack-auth/runtime'",
957
+ * typeName: "AuthRuntimeServices"
958
+ * }
959
+ * ```
960
+ */
961
+ runtimeServiceTypes?: {
962
+ /**
963
+ * Import statement to include in generated types file
964
+ * Must be a complete import statement with 'import type' and quotes
965
+ */
966
+ import: string
967
+ /**
968
+ * TypeScript type name to use in PluginServices interface
969
+ * Should match the exported type from the import
970
+ */
971
+ typeName: string
972
+ }
849
973
  }
850
974
 
851
975
  /**
852
976
  * Main configuration type
977
+ * Using interface instead of type to allow module augmentation
853
978
  */
854
- export type OpenSaasConfig = {
979
+ export interface OpenSaasConfig {
855
980
  db: DatabaseConfig
856
981
  lists: Record<string, ListConfig>
857
982
  session?: SessionConfig
@@ -881,4 +1006,10 @@ export type OpenSaasConfig = {
881
1006
  * @internal
882
1007
  */
883
1008
  _pluginData?: Record<string, unknown>
1009
+ /**
1010
+ * Sorted plugin instances (stored after plugin execution)
1011
+ * Used at runtime to call plugin.runtime() functions
1012
+ * @internal
1013
+ */
1014
+ _plugins?: Plugin[]
884
1015
  }