@opensaas/stack-core 0.1.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 (95) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +447 -0
  3. package/dist/access/engine.d.ts +73 -0
  4. package/dist/access/engine.d.ts.map +1 -0
  5. package/dist/access/engine.js +244 -0
  6. package/dist/access/engine.js.map +1 -0
  7. package/dist/access/field-transforms.d.ts +47 -0
  8. package/dist/access/field-transforms.d.ts.map +1 -0
  9. package/dist/access/field-transforms.js +2 -0
  10. package/dist/access/field-transforms.js.map +1 -0
  11. package/dist/access/index.d.ts +3 -0
  12. package/dist/access/index.d.ts.map +1 -0
  13. package/dist/access/index.js +2 -0
  14. package/dist/access/index.js.map +1 -0
  15. package/dist/access/types.d.ts +83 -0
  16. package/dist/access/types.d.ts.map +1 -0
  17. package/dist/access/types.js +2 -0
  18. package/dist/access/types.js.map +1 -0
  19. package/dist/config/index.d.ts +39 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +38 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/types.d.ts +413 -0
  24. package/dist/config/types.d.ts.map +1 -0
  25. package/dist/config/types.js +2 -0
  26. package/dist/config/types.js.map +1 -0
  27. package/dist/context/index.d.ts +31 -0
  28. package/dist/context/index.d.ts.map +1 -0
  29. package/dist/context/index.js +524 -0
  30. package/dist/context/index.js.map +1 -0
  31. package/dist/context/nested-operations.d.ts +10 -0
  32. package/dist/context/nested-operations.d.ts.map +1 -0
  33. package/dist/context/nested-operations.js +261 -0
  34. package/dist/context/nested-operations.js.map +1 -0
  35. package/dist/fields/index.d.ts +78 -0
  36. package/dist/fields/index.d.ts.map +1 -0
  37. package/dist/fields/index.js +381 -0
  38. package/dist/fields/index.js.map +1 -0
  39. package/dist/hooks/index.d.ts +58 -0
  40. package/dist/hooks/index.d.ts.map +1 -0
  41. package/dist/hooks/index.js +79 -0
  42. package/dist/hooks/index.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +12 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lib/case-utils.d.ts +49 -0
  48. package/dist/lib/case-utils.d.ts.map +1 -0
  49. package/dist/lib/case-utils.js +68 -0
  50. package/dist/lib/case-utils.js.map +1 -0
  51. package/dist/lib/case-utils.test.d.ts +2 -0
  52. package/dist/lib/case-utils.test.d.ts.map +1 -0
  53. package/dist/lib/case-utils.test.js +101 -0
  54. package/dist/lib/case-utils.test.js.map +1 -0
  55. package/dist/utils/password.d.ts +81 -0
  56. package/dist/utils/password.d.ts.map +1 -0
  57. package/dist/utils/password.js +132 -0
  58. package/dist/utils/password.js.map +1 -0
  59. package/dist/validation/schema.d.ts +17 -0
  60. package/dist/validation/schema.d.ts.map +1 -0
  61. package/dist/validation/schema.js +42 -0
  62. package/dist/validation/schema.js.map +1 -0
  63. package/dist/validation/schema.test.d.ts +2 -0
  64. package/dist/validation/schema.test.d.ts.map +1 -0
  65. package/dist/validation/schema.test.js +143 -0
  66. package/dist/validation/schema.test.js.map +1 -0
  67. package/docs/type-distribution-fix.md +136 -0
  68. package/package.json +48 -0
  69. package/src/access/engine.ts +360 -0
  70. package/src/access/field-transforms.ts +99 -0
  71. package/src/access/index.ts +20 -0
  72. package/src/access/types.ts +103 -0
  73. package/src/config/index.ts +71 -0
  74. package/src/config/types.ts +478 -0
  75. package/src/context/index.ts +814 -0
  76. package/src/context/nested-operations.ts +412 -0
  77. package/src/fields/index.ts +438 -0
  78. package/src/hooks/index.ts +132 -0
  79. package/src/index.ts +62 -0
  80. package/src/lib/case-utils.test.ts +127 -0
  81. package/src/lib/case-utils.ts +74 -0
  82. package/src/utils/password.ts +147 -0
  83. package/src/validation/schema.test.ts +171 -0
  84. package/src/validation/schema.ts +59 -0
  85. package/tests/access-relationships.test.ts +613 -0
  86. package/tests/access.test.ts +499 -0
  87. package/tests/config.test.ts +195 -0
  88. package/tests/context.test.ts +248 -0
  89. package/tests/hooks.test.ts +417 -0
  90. package/tests/password-type-distribution.test.ts +155 -0
  91. package/tests/password-types.test.ts +147 -0
  92. package/tests/password.test.ts +249 -0
  93. package/tsconfig.json +12 -0
  94. package/tsconfig.tsbuildinfo +1 -0
  95. package/vitest.config.ts +27 -0
@@ -0,0 +1,438 @@
1
+ import { z } from 'zod'
2
+ import type {
3
+ TextField,
4
+ IntegerField,
5
+ CheckboxField,
6
+ TimestampField,
7
+ PasswordField,
8
+ SelectField,
9
+ RelationshipField,
10
+ } from '../config/types.js'
11
+ import { hashPassword, isHashedPassword, HashedPassword } from '../utils/password.js'
12
+
13
+ /**
14
+ * Format field name for display in error messages
15
+ */
16
+ function formatFieldName(fieldName: string): string {
17
+ return fieldName
18
+ .replace(/([A-Z])/g, ' $1')
19
+ .replace(/^./, (str) => str.toUpperCase())
20
+ .trim()
21
+ }
22
+
23
+ /**
24
+ * Text field
25
+ */
26
+ export function text(options?: Omit<TextField, 'type'>): TextField {
27
+ return {
28
+ type: 'text',
29
+ ...options,
30
+ getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
31
+ const validation = options?.validation
32
+ const isRequired = validation?.isRequired
33
+ const length = validation?.length
34
+ const minLength = length?.min && length.min > 0 ? length.min : 1
35
+
36
+ const baseSchema = z.string({
37
+ message: `${formatFieldName(fieldName)} must be text`,
38
+ })
39
+
40
+ const withMin =
41
+ isRequired || length?.min !== undefined
42
+ ? baseSchema.min(minLength, {
43
+ message:
44
+ minLength > 1
45
+ ? `${formatFieldName(fieldName)} must be at least ${minLength} characters`
46
+ : `${formatFieldName(fieldName)} is required`,
47
+ })
48
+ : baseSchema
49
+
50
+ const withMax =
51
+ length?.max !== undefined
52
+ ? withMin.max(length.max, {
53
+ message: `${formatFieldName(fieldName)} must be at most ${length.max} characters`,
54
+ })
55
+ : withMin
56
+
57
+ if (isRequired && operation === 'update') {
58
+ return z.union([withMax, z.undefined()])
59
+ }
60
+
61
+ return !isRequired ? withMax.optional() : withMax
62
+ },
63
+ getPrismaType: () => {
64
+ const validation = options?.validation
65
+ const isRequired = validation?.isRequired
66
+ let modifiers = ''
67
+
68
+ // Optional modifier
69
+ if (!isRequired) {
70
+ modifiers += '?'
71
+ }
72
+
73
+ // Unique/index modifiers
74
+ if (options?.isIndexed === 'unique') {
75
+ modifiers += ' @unique'
76
+ } else if (options?.isIndexed === true) {
77
+ modifiers += ' @index'
78
+ }
79
+
80
+ return {
81
+ type: 'String',
82
+ modifiers: modifiers || undefined,
83
+ }
84
+ },
85
+ getTypeScriptType: () => {
86
+ const validation = options?.validation
87
+ const isRequired = validation?.isRequired
88
+
89
+ return {
90
+ type: 'string',
91
+ optional: !isRequired,
92
+ }
93
+ },
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Integer field
99
+ */
100
+ export function integer(options?: Omit<IntegerField, 'type'>): IntegerField {
101
+ return {
102
+ type: 'integer',
103
+ ...options,
104
+ getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
105
+ const baseSchema = z.number({
106
+ message: `${formatFieldName(fieldName)} must be a number`,
107
+ })
108
+
109
+ const withMin =
110
+ options?.validation?.min !== undefined
111
+ ? baseSchema.min(options.validation.min, {
112
+ message: `${formatFieldName(fieldName)} must be at least ${options.validation.min}`,
113
+ })
114
+ : baseSchema
115
+
116
+ const withMax =
117
+ options?.validation?.max !== undefined
118
+ ? withMin.max(options.validation.max, {
119
+ message: `${formatFieldName(fieldName)} must be at most ${options.validation.max}`,
120
+ })
121
+ : withMin
122
+
123
+ return !options?.validation?.isRequired || operation === 'update'
124
+ ? withMax.optional()
125
+ : withMax
126
+ },
127
+ getPrismaType: () => {
128
+ const isRequired = options?.validation?.isRequired
129
+
130
+ return {
131
+ type: 'Int',
132
+ modifiers: isRequired ? undefined : '?',
133
+ }
134
+ },
135
+ getTypeScriptType: () => {
136
+ const isRequired = options?.validation?.isRequired
137
+
138
+ return {
139
+ type: 'number',
140
+ optional: !isRequired,
141
+ }
142
+ },
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Checkbox (boolean) field
148
+ */
149
+ export function checkbox(options?: Omit<CheckboxField, 'type'>): CheckboxField {
150
+ return {
151
+ type: 'checkbox',
152
+ ...options,
153
+ getZodSchema: () => {
154
+ return z.boolean().optional()
155
+ },
156
+ getPrismaType: () => {
157
+ const hasDefault = options?.defaultValue !== undefined
158
+ let modifiers = ''
159
+
160
+ if (hasDefault) {
161
+ modifiers = ` @default(${options.defaultValue})`
162
+ }
163
+
164
+ return {
165
+ type: 'Boolean',
166
+ modifiers: modifiers || undefined,
167
+ }
168
+ },
169
+ getTypeScriptType: () => {
170
+ return {
171
+ type: 'boolean',
172
+ optional: options?.defaultValue === undefined,
173
+ }
174
+ },
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Timestamp (DateTime) field
180
+ */
181
+ export function timestamp(options?: Omit<TimestampField, 'type'>): TimestampField {
182
+ return {
183
+ type: 'timestamp',
184
+ ...options,
185
+ getZodSchema: () => {
186
+ return z.union([z.date(), z.iso.datetime()]).optional()
187
+ },
188
+ getPrismaType: () => {
189
+ let modifiers = '?'
190
+
191
+ // Check for default value
192
+ if (
193
+ options?.defaultValue &&
194
+ typeof options.defaultValue === 'object' &&
195
+ 'kind' in options.defaultValue &&
196
+ options.defaultValue.kind === 'now'
197
+ ) {
198
+ modifiers = ' @default(now())'
199
+ }
200
+
201
+ return {
202
+ type: 'DateTime',
203
+ modifiers,
204
+ }
205
+ },
206
+ getTypeScriptType: () => {
207
+ const hasDefault =
208
+ options?.defaultValue &&
209
+ typeof options.defaultValue === 'object' &&
210
+ 'kind' in options.defaultValue &&
211
+ options.defaultValue.kind === 'now'
212
+
213
+ return {
214
+ type: 'Date',
215
+ optional: !hasDefault,
216
+ }
217
+ },
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Password field (automatically hashed using bcrypt)
223
+ *
224
+ * **Security Features:**
225
+ * - Passwords are automatically hashed during create/update operations
226
+ * - Uses bcrypt with cost factor 10 (good balance of security and performance)
227
+ * - Already-hashed passwords are not re-hashed (idempotent)
228
+ * - Password values in query results include a `compare()` method for authentication
229
+ *
230
+ * **Usage Example:**
231
+ * ```typescript
232
+ * // In opensaas.config.ts
233
+ * fields: {
234
+ * password: password({
235
+ * validation: { isRequired: true }
236
+ * })
237
+ * }
238
+ *
239
+ * // Creating a user - password is automatically hashed
240
+ * const user = await context.db.user.create({
241
+ * data: {
242
+ * email: 'user@example.com',
243
+ * password: 'plaintextPassword' // Automatically hashed before storage
244
+ * }
245
+ * })
246
+ *
247
+ * // Authenticating - use the compare() method
248
+ * const user = await context.db.user.findUnique({
249
+ * where: { email: 'user@example.com' }
250
+ * })
251
+ *
252
+ * if (user && await user.password.compare('plaintextPassword')) {
253
+ * // Password is correct - login successful
254
+ * }
255
+ * ```
256
+ *
257
+ * **Important Notes:**
258
+ * - Password fields are excluded from read operations by default in access control
259
+ * - Always use the `compare()` method to verify passwords - never compare strings directly
260
+ * - The password field value has type `HashedPassword` which extends string with compare()
261
+ * - Empty strings and undefined values are skipped (not hashed) to allow partial updates
262
+ *
263
+ * **Implementation Details:**
264
+ * - Uses field-level hooks (`resolveInput` and `resolveOutput`) for automatic transformations
265
+ * - The hashing happens via `hooks.resolveInput` during create/update operations
266
+ * - The wrapping happens via `hooks.resolveOutput` during read operations
267
+ * - This pattern allows third-party field types to define their own transformations
268
+ *
269
+ * @param options - Field configuration options
270
+ * @returns Password field configuration
271
+ */
272
+ export function password(options?: Omit<PasswordField, 'type'>): PasswordField {
273
+ return {
274
+ type: 'password',
275
+ ...options,
276
+ typePatch: {
277
+ resultType: "import('@opensaas/stack-core').HashedPassword",
278
+ patchScope: 'scalars-only',
279
+ },
280
+ ui: {
281
+ ...options?.ui,
282
+ valueForClientSerialization: ({ value }) => ({ isSet: !!value }),
283
+ },
284
+ hooks: {
285
+ // Hash password before writing to database
286
+ resolveInput: async ({ inputValue }) => {
287
+ // Skip if undefined or null (allows partial updates)
288
+ if (inputValue === undefined || inputValue === null) {
289
+ return inputValue
290
+ }
291
+
292
+ // Skip if not a string
293
+ if (typeof inputValue !== 'string') {
294
+ return inputValue
295
+ }
296
+
297
+ // Skip empty strings (let validation handle this)
298
+ if (inputValue.length === 0) {
299
+ return inputValue
300
+ }
301
+
302
+ // Skip if already hashed (idempotent)
303
+ if (isHashedPassword(inputValue)) {
304
+ return inputValue
305
+ }
306
+
307
+ // Hash the password
308
+ return await hashPassword(inputValue)
309
+ },
310
+ // Wrap password with HashedPassword class after reading from database
311
+ resolveOutput: ({ value }) => {
312
+ // Only wrap string values (hashed passwords)
313
+ if (typeof value === 'string' && value.length > 0) {
314
+ return new HashedPassword(value)
315
+ }
316
+ return undefined
317
+ },
318
+ // Merge with user-provided hooks if any
319
+ ...options?.hooks,
320
+ },
321
+ getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
322
+ const validation = options?.validation
323
+ const isRequired = validation?.isRequired
324
+
325
+ if (isRequired && operation === 'create') {
326
+ // Required in create mode: reject undefined and empty strings
327
+ return z
328
+ .string({
329
+ message: `${formatFieldName(fieldName)} must be text`,
330
+ })
331
+ .min(1, {
332
+ message: `${formatFieldName(fieldName)} is required`,
333
+ })
334
+ } else if (isRequired && operation === 'update') {
335
+ // Required in update mode: if provided, reject empty strings
336
+ return z.union([
337
+ z.string().min(1, {
338
+ message: `${formatFieldName(fieldName)} is required`,
339
+ }),
340
+ z.undefined(),
341
+ ])
342
+ } else {
343
+ // Not required: can be undefined or any string
344
+ return z
345
+ .string({
346
+ message: `${formatFieldName(fieldName)} must be text`,
347
+ })
348
+ .optional()
349
+ }
350
+ },
351
+ getPrismaType: () => {
352
+ const isRequired = options?.validation?.isRequired
353
+
354
+ return {
355
+ type: 'String',
356
+ modifiers: isRequired ? undefined : '?',
357
+ }
358
+ },
359
+ getTypeScriptType: () => {
360
+ const isRequired = options?.validation?.isRequired
361
+
362
+ return {
363
+ type: 'string',
364
+ optional: !isRequired,
365
+ }
366
+ },
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Select field (enum-like)
372
+ */
373
+ export function select(options: Omit<SelectField, 'type'>): SelectField {
374
+ if (!options.options || options.options.length === 0) {
375
+ throw new Error('Select field must have at least one option')
376
+ }
377
+
378
+ return {
379
+ type: 'select',
380
+ ...options,
381
+ getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
382
+ const values = options.options.map((opt) => opt.value)
383
+ let schema: z.ZodTypeAny = z.enum(values as [string, ...string[]], {
384
+ message: `${formatFieldName(fieldName)} must be one of: ${values.join(', ')}`,
385
+ })
386
+
387
+ if (!options.validation?.isRequired || operation === 'update') {
388
+ schema = schema.optional()
389
+ }
390
+
391
+ return schema
392
+ },
393
+ getPrismaType: () => {
394
+ let modifiers = '?'
395
+
396
+ // Add default value if provided
397
+ if (options.defaultValue !== undefined) {
398
+ modifiers = ` @default("${options.defaultValue}")`
399
+ }
400
+
401
+ return {
402
+ type: 'String',
403
+ modifiers,
404
+ }
405
+ },
406
+ getTypeScriptType: () => {
407
+ // Generate union type from options
408
+ const unionType = options.options.map((opt) => `'${opt.value}'`).join(' | ')
409
+
410
+ return {
411
+ type: unionType,
412
+ optional: !options.validation?.isRequired || options.defaultValue !== undefined,
413
+ }
414
+ },
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Relationship field
420
+ */
421
+ export function relationship(options: Omit<RelationshipField, 'type'>): RelationshipField {
422
+ if (!options.ref) {
423
+ throw new Error('Relationship field must have a ref')
424
+ }
425
+
426
+ // Validate ref format: 'ListName.fieldName'
427
+ const refParts = options.ref.split('.')
428
+ if (refParts.length !== 2) {
429
+ throw new Error(
430
+ `Invalid relationship ref format: "${options.ref}". Expected format: "ListName.fieldName"`,
431
+ )
432
+ }
433
+
434
+ return {
435
+ type: 'relationship',
436
+ ...options,
437
+ }
438
+ }
@@ -0,0 +1,132 @@
1
+ import type { Hooks } from '../config/types.js'
2
+ import type { AccessContext } from '../access/types.js'
3
+ import type { FieldConfig } from '../config/types.js'
4
+ import { validateWithZod } from '../validation/schema.js'
5
+
6
+ /**
7
+ * Validation error collection
8
+ */
9
+ export class ValidationError extends Error {
10
+ public errors: string[]
11
+ public fieldErrors: Record<string, string>
12
+
13
+ constructor(errors: string[], fieldErrors: Record<string, string> = {}) {
14
+ super(`Validation failed: ${errors.join(', ')}`)
15
+ this.name = 'ValidationError'
16
+ this.errors = errors
17
+ this.fieldErrors = fieldErrors
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Execute resolveInput hook
23
+ * Allows modification of input data before validation
24
+ */
25
+ export async function executeResolveInput<T = Record<string, unknown>>(
26
+ hooks: Hooks<T> | undefined,
27
+ args: {
28
+ operation: 'create' | 'update'
29
+ resolvedData: Partial<T>
30
+ item?: T
31
+ context: AccessContext
32
+ },
33
+ ): Promise<Partial<T>> {
34
+ if (!hooks?.resolveInput) {
35
+ return args.resolvedData
36
+ }
37
+
38
+ const result = await hooks.resolveInput(args)
39
+ return result
40
+ }
41
+
42
+ /**
43
+ * Execute validateInput hook
44
+ * Allows custom validation logic
45
+ */
46
+ export async function executeValidateInput<T = Record<string, unknown>>(
47
+ hooks: Hooks<T> | undefined,
48
+ args: {
49
+ operation: 'create' | 'update'
50
+ resolvedData: Partial<T>
51
+ item?: T
52
+ context: AccessContext
53
+ },
54
+ ): Promise<void> {
55
+ if (!hooks?.validateInput) {
56
+ return
57
+ }
58
+
59
+ const errors: string[] = []
60
+
61
+ const addValidationError = (msg: string) => {
62
+ errors.push(msg)
63
+ }
64
+
65
+ await hooks.validateInput({
66
+ ...args,
67
+ addValidationError,
68
+ })
69
+
70
+ if (errors.length > 0) {
71
+ throw new ValidationError(errors)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Execute beforeOperation hook
77
+ * Runs before database operation (cannot modify data)
78
+ */
79
+ export async function executeBeforeOperation<T = Record<string, unknown>>(
80
+ hooks: Hooks<T> | undefined,
81
+ args: {
82
+ operation: 'create' | 'update' | 'delete'
83
+ item?: T
84
+ context: AccessContext
85
+ },
86
+ ): Promise<void> {
87
+ if (!hooks?.beforeOperation) {
88
+ return
89
+ }
90
+
91
+ await hooks.beforeOperation(args)
92
+ }
93
+
94
+ /**
95
+ * Execute afterOperation hook
96
+ * Runs after database operation
97
+ */
98
+ export async function executeAfterOperation<T = Record<string, unknown>>(
99
+ hooks: Hooks<T> | undefined,
100
+ args: {
101
+ operation: 'create' | 'update' | 'delete'
102
+ item: T
103
+ context: AccessContext
104
+ },
105
+ ): Promise<void> {
106
+ if (!hooks?.afterOperation) {
107
+ return
108
+ }
109
+
110
+ await hooks.afterOperation(args)
111
+ }
112
+
113
+ /**
114
+ * Validate field-level validation rules using Zod
115
+ * Checks isRequired, length constraints, etc.
116
+ */
117
+ export function validateFieldRules(
118
+ data: Record<string, unknown>,
119
+ fieldConfigs: Record<string, FieldConfig>,
120
+ operation: 'create' | 'update' = 'create',
121
+ ): { errors: string[]; fieldErrors: Record<string, string> } {
122
+ const result = validateWithZod(data, fieldConfigs, operation)
123
+
124
+ if (result.success) {
125
+ return { errors: [], fieldErrors: {} }
126
+ }
127
+
128
+ // Convert field errors to array of error messages
129
+ const errors = Object.entries(result.errors).map(([_field, message]) => message)
130
+
131
+ return { errors, fieldErrors: result.errors }
132
+ }
package/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // Config system
2
+ export { config, list } from './config/index.js'
3
+ export type {
4
+ OpenSaasConfig,
5
+ ListConfig,
6
+ FieldConfig,
7
+ BaseFieldConfig,
8
+ TextField,
9
+ IntegerField,
10
+ CheckboxField,
11
+ TimestampField,
12
+ PasswordField,
13
+ SelectField,
14
+ RelationshipField,
15
+ OperationAccess,
16
+ Hooks,
17
+ FieldHooks,
18
+ DatabaseConfig,
19
+ SessionConfig,
20
+ UIConfig,
21
+ ThemeConfig,
22
+ ThemePreset,
23
+ ThemeColors,
24
+ } from './config/index.js'
25
+
26
+ // Access control
27
+ export type {
28
+ AccessControl,
29
+ FieldAccess,
30
+ Session,
31
+ AccessContext,
32
+ PrismaFilter,
33
+ AccessControlledDB,
34
+ } from './access/index.js'
35
+
36
+ // Context
37
+ export { getContext } from './context/index.js'
38
+ export type { PrismaClientLike } from './access/types.js'
39
+ export type { ServerActionProps } from './context/index.js'
40
+
41
+ // Utilities
42
+ export {
43
+ getDbKey,
44
+ getUrlKey,
45
+ getListKeyFromUrl,
46
+ pascalToCamel,
47
+ pascalToKebab,
48
+ kebabToPascal,
49
+ kebabToCamel,
50
+ } from './lib/case-utils.js'
51
+
52
+ // Hooks and validation
53
+ export { ValidationError } from './hooks/index.js'
54
+ export { validateWithZod, generateZodSchema } from './validation/schema.js'
55
+
56
+ // Password utilities
57
+ export {
58
+ hashPassword,
59
+ comparePassword,
60
+ isHashedPassword,
61
+ HashedPassword,
62
+ } from './utils/password.js'