@opensaas/stack-core 0.38.0 → 0.39.1

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 (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +125 -0
  3. package/CLAUDE.md +21 -0
  4. package/dist/access/access-filter.d.ts +46 -1
  5. package/dist/access/access-filter.d.ts.map +1 -1
  6. package/dist/access/access-filter.js +113 -4
  7. package/dist/access/access-filter.js.map +1 -1
  8. package/dist/access/declared-dependencies.d.ts.map +1 -1
  9. package/dist/access/declared-dependencies.js +4 -9
  10. package/dist/access/declared-dependencies.js.map +1 -1
  11. package/dist/access/engine.d.ts +0 -20
  12. package/dist/access/engine.d.ts.map +1 -1
  13. package/dist/access/engine.js +0 -25
  14. package/dist/access/engine.js.map +1 -1
  15. package/dist/access/errors.d.ts +49 -0
  16. package/dist/access/errors.d.ts.map +1 -1
  17. package/dist/access/errors.js +75 -0
  18. package/dist/access/errors.js.map +1 -1
  19. package/dist/access/field-access.d.ts +39 -1
  20. package/dist/access/field-access.d.ts.map +1 -1
  21. package/dist/access/field-access.js +108 -42
  22. package/dist/access/field-access.js.map +1 -1
  23. package/dist/access/field-access.test.js +229 -2
  24. package/dist/access/field-access.test.js.map +1 -1
  25. package/dist/access/field-transforms.d.ts +0 -27
  26. package/dist/access/field-transforms.d.ts.map +1 -1
  27. package/dist/access/field-visibility.d.ts +0 -4
  28. package/dist/access/field-visibility.d.ts.map +1 -1
  29. package/dist/access/field-visibility.js +13 -39
  30. package/dist/access/field-visibility.js.map +1 -1
  31. package/dist/access/index.d.ts +6 -2
  32. package/dist/access/index.d.ts.map +1 -1
  33. package/dist/access/index.js +12 -2
  34. package/dist/access/index.js.map +1 -1
  35. package/dist/access/query-validation.d.ts +121 -0
  36. package/dist/access/query-validation.d.ts.map +1 -0
  37. package/dist/access/query-validation.js +292 -0
  38. package/dist/access/query-validation.js.map +1 -0
  39. package/dist/access/relationship-count.d.ts.map +1 -1
  40. package/dist/access/relationship-count.js +2 -10
  41. package/dist/access/relationship-count.js.map +1 -1
  42. package/dist/access/relationship-label-filter.d.ts +23 -23
  43. package/dist/access/relationship-label-filter.d.ts.map +1 -1
  44. package/dist/access/relationship-label-filter.js +5 -76
  45. package/dist/access/relationship-label-filter.js.map +1 -1
  46. package/dist/access/relationship-label-filter.test.js +19 -56
  47. package/dist/access/relationship-label-filter.test.js.map +1 -1
  48. package/dist/access/transaction-registry.d.ts +45 -0
  49. package/dist/access/transaction-registry.d.ts.map +1 -0
  50. package/dist/access/transaction-registry.js +37 -0
  51. package/dist/access/transaction-registry.js.map +1 -0
  52. package/dist/access/types.d.ts +55 -59
  53. package/dist/access/types.d.ts.map +1 -1
  54. package/dist/config/index.d.ts +9 -7
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/config/index.js +10 -18
  57. package/dist/config/index.js.map +1 -1
  58. package/dist/config/label.d.ts +6 -13
  59. package/dist/config/label.d.ts.map +1 -1
  60. package/dist/config/label.js +6 -13
  61. package/dist/config/label.js.map +1 -1
  62. package/dist/config/nav-count.d.ts +11 -19
  63. package/dist/config/nav-count.d.ts.map +1 -1
  64. package/dist/config/nav-count.js +11 -19
  65. package/dist/config/nav-count.js.map +1 -1
  66. package/dist/config/plugin-engine.d.ts +0 -10
  67. package/dist/config/plugin-engine.d.ts.map +1 -1
  68. package/dist/config/plugin-engine.js +1 -37
  69. package/dist/config/plugin-engine.js.map +1 -1
  70. package/dist/config/types.d.ts +94 -109
  71. package/dist/config/types.d.ts.map +1 -1
  72. package/dist/context/apply-defaults.d.ts +8 -31
  73. package/dist/context/apply-defaults.d.ts.map +1 -1
  74. package/dist/context/apply-defaults.js +12 -41
  75. package/dist/context/apply-defaults.js.map +1 -1
  76. package/dist/context/hook-pipeline.d.ts +4 -29
  77. package/dist/context/hook-pipeline.d.ts.map +1 -1
  78. package/dist/context/hook-pipeline.js +8 -41
  79. package/dist/context/hook-pipeline.js.map +1 -1
  80. package/dist/context/index.d.ts +4 -15
  81. package/dist/context/index.d.ts.map +1 -1
  82. package/dist/context/index.js +190 -193
  83. package/dist/context/index.js.map +1 -1
  84. package/dist/context/nested-operations.d.ts +7 -26
  85. package/dist/context/nested-operations.d.ts.map +1 -1
  86. package/dist/context/nested-operations.js +73 -171
  87. package/dist/context/nested-operations.js.map +1 -1
  88. package/dist/context/transaction-boundary.d.ts +36 -47
  89. package/dist/context/transaction-boundary.d.ts.map +1 -1
  90. package/dist/context/transaction-boundary.js +92 -79
  91. package/dist/context/transaction-boundary.js.map +1 -1
  92. package/dist/context/write-pipeline.d.ts +25 -64
  93. package/dist/context/write-pipeline.d.ts.map +1 -1
  94. package/dist/context/write-pipeline.js +81 -111
  95. package/dist/context/write-pipeline.js.map +1 -1
  96. package/dist/fields/format-prisma-default.d.ts.map +1 -1
  97. package/dist/fields/format-prisma-default.js +4 -7
  98. package/dist/fields/format-prisma-default.js.map +1 -1
  99. package/dist/fields/index.d.ts +17 -2
  100. package/dist/fields/index.d.ts.map +1 -1
  101. package/dist/fields/index.js +136 -147
  102. package/dist/fields/index.js.map +1 -1
  103. package/dist/filter/collect.d.ts +26 -3
  104. package/dist/filter/collect.d.ts.map +1 -1
  105. package/dist/filter/collect.js +30 -12
  106. package/dist/filter/collect.js.map +1 -1
  107. package/dist/filter/filter.test.js +98 -15
  108. package/dist/filter/filter.test.js.map +1 -1
  109. package/dist/filter/index.d.ts +1 -0
  110. package/dist/filter/index.d.ts.map +1 -1
  111. package/dist/filter/index.js.map +1 -1
  112. package/dist/filter/map.d.ts.map +1 -1
  113. package/dist/filter/map.js +0 -5
  114. package/dist/filter/map.js.map +1 -1
  115. package/dist/filter/parse.d.ts.map +1 -1
  116. package/dist/filter/parse.js +2 -9
  117. package/dist/filter/parse.js.map +1 -1
  118. package/dist/filter/serialize.d.ts.map +1 -1
  119. package/dist/filter/serialize.js +2 -4
  120. package/dist/filter/serialize.js.map +1 -1
  121. package/dist/hooks/index.d.ts +4 -42
  122. package/dist/hooks/index.d.ts.map +1 -1
  123. package/dist/hooks/index.js +5 -59
  124. package/dist/hooks/index.js.map +1 -1
  125. package/dist/index.d.ts +3 -1
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +20 -4
  128. package/dist/index.js.map +1 -1
  129. package/dist/internal.d.ts +1 -0
  130. package/dist/internal.d.ts.map +1 -1
  131. package/dist/internal.js +4 -0
  132. package/dist/internal.js.map +1 -1
  133. package/dist/lib/case-utils.d.ts +0 -32
  134. package/dist/lib/case-utils.d.ts.map +1 -1
  135. package/dist/lib/case-utils.js +0 -32
  136. package/dist/lib/case-utils.js.map +1 -1
  137. package/dist/mcp/handler.d.ts +0 -4
  138. package/dist/mcp/handler.d.ts.map +1 -1
  139. package/dist/mcp/handler.js +12 -69
  140. package/dist/mcp/handler.js.map +1 -1
  141. package/dist/query/index.d.ts +26 -227
  142. package/dist/query/index.d.ts.map +1 -1
  143. package/dist/query/index.js +14 -117
  144. package/dist/query/index.js.map +1 -1
  145. package/dist/utils/password.d.ts +0 -68
  146. package/dist/utils/password.d.ts.map +1 -1
  147. package/dist/utils/password.js +7 -76
  148. package/dist/utils/password.js.map +1 -1
  149. package/dist/validation/field-config.d.ts.map +1 -1
  150. package/dist/validation/field-config.js +0 -6
  151. package/dist/validation/field-config.js.map +1 -1
  152. package/dist/validation/schema.d.ts +0 -7
  153. package/dist/validation/schema.d.ts.map +1 -1
  154. package/dist/validation/schema.js +0 -10
  155. package/dist/validation/schema.js.map +1 -1
  156. package/package.json +1 -1
  157. package/src/access/access-filter.ts +152 -5
  158. package/src/access/declared-dependencies.ts +4 -9
  159. package/src/access/engine.ts +0 -25
  160. package/src/access/errors.ts +81 -0
  161. package/src/access/field-access.test.ts +312 -2
  162. package/src/access/field-access.ts +118 -42
  163. package/src/access/field-transforms.ts +8 -39
  164. package/src/access/field-visibility.ts +16 -47
  165. package/src/access/index.ts +20 -2
  166. package/src/access/query-validation.ts +359 -0
  167. package/src/access/relationship-count.ts +2 -10
  168. package/src/access/relationship-label-filter.test.ts +19 -68
  169. package/src/access/relationship-label-filter.ts +27 -104
  170. package/src/access/transaction-registry.ts +58 -0
  171. package/src/access/types.ts +83 -86
  172. package/src/config/index.ts +10 -22
  173. package/src/config/label.ts +6 -13
  174. package/src/config/nav-count.ts +11 -19
  175. package/src/config/plugin-engine.ts +1 -37
  176. package/src/config/types.ts +95 -110
  177. package/src/context/apply-defaults.ts +12 -44
  178. package/src/context/hook-pipeline.ts +12 -67
  179. package/src/context/index.ts +263 -247
  180. package/src/context/nested-operations.ts +94 -226
  181. package/src/context/transaction-boundary.ts +113 -109
  182. package/src/context/write-pipeline.ts +99 -155
  183. package/src/fields/format-prisma-default.ts +4 -7
  184. package/src/fields/index.ts +155 -147
  185. package/src/filter/collect.ts +48 -12
  186. package/src/filter/filter.test.ts +153 -15
  187. package/src/filter/index.ts +1 -0
  188. package/src/filter/map.ts +0 -5
  189. package/src/filter/parse.ts +2 -9
  190. package/src/filter/serialize.ts +2 -4
  191. package/src/hooks/index.ts +5 -59
  192. package/src/index.ts +23 -4
  193. package/src/internal.ts +5 -0
  194. package/src/lib/case-utils.ts +0 -32
  195. package/src/mcp/handler.ts +13 -71
  196. package/src/query/index.ts +29 -235
  197. package/src/utils/password.ts +7 -76
  198. package/src/validation/field-config.ts +0 -6
  199. package/src/validation/schema.ts +0 -10
  200. package/tests/context.test.ts +700 -1
  201. package/tests/field-types.test.ts +222 -0
  202. package/tests/mcp-handler.test.ts +34 -0
  203. package/tests/transaction-boundary-outermost.test.ts +530 -0
  204. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import { describe, test, expect } from 'vitest'
2
2
  import {
3
3
  text,
4
4
  integer,
5
+ bigInt,
5
6
  checkbox,
6
7
  timestamp,
7
8
  password,
@@ -327,6 +328,227 @@ describe('Field Types', () => {
327
328
  })
328
329
  })
329
330
 
331
+ describe('bigInt field', () => {
332
+ describe('getZodSchema', () => {
333
+ test('returns optional bigint schema for non-required field', () => {
334
+ const field = bigInt()
335
+ const schema = field.getZodSchema('epoch', 'create')
336
+
337
+ expect(schema.safeParse(25n).success).toBe(true)
338
+ expect(schema.safeParse(undefined).success).toBe(true)
339
+ })
340
+
341
+ test('returns required schema for required field in create mode', () => {
342
+ const field = bigInt({ validation: { isRequired: true } })
343
+ const schema = field.getZodSchema('epoch', 'create')
344
+
345
+ expect(schema.safeParse(25n).success).toBe(true)
346
+ expect(schema.safeParse(undefined).success).toBe(false)
347
+ })
348
+
349
+ test('returns optional schema for required field in update mode', () => {
350
+ const field = bigInt({ validation: { isRequired: true } })
351
+ const schema = field.getZodSchema('epoch', 'update')
352
+
353
+ expect(schema.safeParse(25n).success).toBe(true)
354
+ expect(schema.safeParse(undefined).success).toBe(true)
355
+ })
356
+
357
+ test('coerces a bigint input to a bigint output unchanged', () => {
358
+ const field = bigInt()
359
+ const schema = field.getZodSchema('epoch', 'create')
360
+
361
+ const result = schema.safeParse(9007199254740993n)
362
+ expect(result.success).toBe(true)
363
+ if (result.success) {
364
+ expect(result.data).toBe(9007199254740993n)
365
+ expect(typeof result.data).toBe('bigint')
366
+ }
367
+ })
368
+
369
+ test('coerces a safe-integer number input to a bigint output', () => {
370
+ const field = bigInt()
371
+ const schema = field.getZodSchema('epoch', 'create')
372
+
373
+ const result = schema.safeParse(1700000000000)
374
+ expect(result.success).toBe(true)
375
+ if (result.success) {
376
+ expect(result.data).toBe(1700000000000n)
377
+ expect(typeof result.data).toBe('bigint')
378
+ }
379
+ })
380
+
381
+ test('coerces a numeric string input to a bigint output, round-tripping beyond Number.MAX_SAFE_INTEGER', () => {
382
+ const field = bigInt()
383
+ const schema = field.getZodSchema('epoch', 'create')
384
+
385
+ const result = schema.safeParse('9223372036854775807')
386
+ expect(result.success).toBe(true)
387
+ if (result.success) {
388
+ expect(result.data).toBe(9223372036854775807n)
389
+ }
390
+ })
391
+
392
+ test('accepts a negative numeric string', () => {
393
+ const field = bigInt()
394
+ const schema = field.getZodSchema('delta', 'create')
395
+
396
+ const result = schema.safeParse('-42')
397
+ expect(result.success).toBe(true)
398
+ if (result.success) {
399
+ expect(result.data).toBe(-42n)
400
+ }
401
+ })
402
+
403
+ test('rejects a non-integer number', () => {
404
+ const field = bigInt()
405
+ const schema = field.getZodSchema('epoch', 'create')
406
+
407
+ expect(schema.safeParse(1.5).success).toBe(false)
408
+ })
409
+
410
+ test('rejects a number above Number.MAX_SAFE_INTEGER instead of silently losing precision', () => {
411
+ const field = bigInt()
412
+ const schema = field.getZodSchema('epoch', 'create')
413
+
414
+ expect(schema.safeParse(Number.MAX_SAFE_INTEGER + 10).success).toBe(false)
415
+ })
416
+
417
+ test('rejects a non-numeric string', () => {
418
+ const field = bigInt()
419
+ const schema = field.getZodSchema('epoch', 'create')
420
+
421
+ expect(schema.safeParse('not-a-number').success).toBe(false)
422
+ })
423
+
424
+ test('rejects a decimal string', () => {
425
+ const field = bigInt()
426
+ const schema = field.getZodSchema('epoch', 'create')
427
+
428
+ expect(schema.safeParse('1.5').success).toBe(false)
429
+ })
430
+
431
+ test('validates min value', () => {
432
+ const field = bigInt({ validation: { min: 0n } })
433
+ const schema = field.getZodSchema('epoch', 'create')
434
+
435
+ expect(schema.safeParse(-1n).success).toBe(false)
436
+ expect(schema.safeParse(0n).success).toBe(true)
437
+ expect(schema.safeParse(1n).success).toBe(true)
438
+ })
439
+
440
+ test('validates max value', () => {
441
+ const field = bigInt({ validation: { max: 100n } })
442
+ const schema = field.getZodSchema('epoch', 'create')
443
+
444
+ expect(schema.safeParse(99n).success).toBe(true)
445
+ expect(schema.safeParse(100n).success).toBe(true)
446
+ expect(schema.safeParse(101n).success).toBe(false)
447
+ })
448
+
449
+ test('validates min and max together', () => {
450
+ const field = bigInt({ validation: { min: 18n, max: 65n } })
451
+ const schema = field.getZodSchema('age', 'create')
452
+
453
+ expect(schema.safeParse(17n).success).toBe(false)
454
+ expect(schema.safeParse(18n).success).toBe(true)
455
+ expect(schema.safeParse(65n).success).toBe(true)
456
+ expect(schema.safeParse(66n).success).toBe(false)
457
+ })
458
+ })
459
+
460
+ describe('getPrismaType', () => {
461
+ test('returns BigInt type for basic field', () => {
462
+ const field = bigInt()
463
+ const prismaType = field.getPrismaType('epoch')
464
+
465
+ expect(prismaType.type).toBe('BigInt')
466
+ expect(prismaType.modifiers).toBe('?')
467
+ })
468
+
469
+ test('returns required BigInt for required field', () => {
470
+ const field = bigInt({ validation: { isRequired: true } })
471
+ const prismaType = field.getPrismaType('epoch')
472
+
473
+ expect(prismaType.type).toBe('BigInt')
474
+ expect(prismaType.modifiers).toBeUndefined()
475
+ })
476
+
477
+ test('db.isNullable: false makes field non-nullable regardless of validation', () => {
478
+ const field = bigInt({ db: { isNullable: false } })
479
+ const prismaType = field.getPrismaType('epoch')
480
+
481
+ expect(prismaType.modifiers).toBeUndefined()
482
+ })
483
+
484
+ test('db.map generates @map attribute', () => {
485
+ const field = bigInt({ db: { map: 'occurred_at_ms' } })
486
+ const prismaType = field.getPrismaType('epoch')
487
+
488
+ expect(prismaType.modifiers).toContain('@map("occurred_at_ms")')
489
+ })
490
+
491
+ test('defaultValue generates a bare @default literal', () => {
492
+ const field = bigInt({ defaultValue: 0n })
493
+ const prismaType = field.getPrismaType('epoch')
494
+
495
+ expect(prismaType.modifiers).toContain('@default(0)')
496
+ })
497
+
498
+ test('isIndexed: true requests a block-level index', () => {
499
+ const field = bigInt({ isIndexed: true })
500
+ const prismaType = field.getPrismaType('epoch')
501
+
502
+ expect(prismaType.index).toBe(true)
503
+ })
504
+
505
+ test("isIndexed: 'unique' generates @unique modifier", () => {
506
+ const field = bigInt({ isIndexed: 'unique' })
507
+ const prismaType = field.getPrismaType('epoch')
508
+
509
+ expect(prismaType.modifiers).toContain('@unique')
510
+ })
511
+ })
512
+
513
+ describe('getTypeScriptType', () => {
514
+ test('returns optional bigint type for non-required field', () => {
515
+ const field = bigInt()
516
+ const tsType = field.getTypeScriptType()
517
+
518
+ expect(tsType.type).toBe('bigint')
519
+ expect(tsType.optional).toBe(true)
520
+ })
521
+
522
+ test('returns required bigint type for required field', () => {
523
+ const field = bigInt({ validation: { isRequired: true } })
524
+ const tsType = field.getTypeScriptType()
525
+
526
+ expect(tsType.type).toBe('bigint')
527
+ expect(tsType.optional).toBe(false)
528
+ })
529
+ })
530
+
531
+ describe('getFilterSpec', () => {
532
+ test('parses a valid integer token into a BigInt condition', () => {
533
+ const field = bigInt()
534
+ const spec = field.getFilterSpec?.('epoch', 'Event', {} as never)
535
+
536
+ expect(spec?.toCondition('eq', '9007199254740993')).toEqual({
537
+ epoch: { equals: 9007199254740993n },
538
+ })
539
+ expect(spec?.toCondition('gt', '5')).toEqual({ epoch: { gt: 5n } })
540
+ })
541
+
542
+ test('degrades to free text (null) for a non-integer token', () => {
543
+ const field = bigInt()
544
+ const spec = field.getFilterSpec?.('epoch', 'Event', {} as never)
545
+
546
+ expect(spec?.toCondition('eq', '1.5')).toBeNull()
547
+ expect(spec?.toCondition('eq', 'not-a-number')).toBeNull()
548
+ })
549
+ })
550
+ })
551
+
330
552
  describe('checkbox field', () => {
331
553
  describe('getZodSchema', () => {
332
554
  test('returns optional boolean schema', () => {
@@ -326,6 +326,40 @@ describe('MCP Handler', () => {
326
326
  })
327
327
  })
328
328
 
329
+ // Regression test (issue #907 / ADR-0029): a bare `JSON.stringify` throws
330
+ // `TypeError: Do not know how to serialize a BigInt` on a bigInt() field's
331
+ // value. This must respond successfully, rendering the value as a decimal
332
+ // string — the field's MCP wire representation.
333
+ it('should serialize a bigint field value as a decimal string instead of throwing', async () => {
334
+ const mockResults = [{ id: '1', title: 'Post 1', occurredAtMs: 9007199254740993n }]
335
+ mockPrisma.post.findMany.mockResolvedValue(mockResults)
336
+
337
+ const handlers = createMcpHandlers({ config, getSession: mockGetSession, getContext })
338
+
339
+ const request = new Request('http://localhost/api/mcp', {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({
343
+ jsonrpc: '2.0',
344
+ id: 1,
345
+ method: 'tools/call',
346
+ params: {
347
+ name: 'list_post_query',
348
+ arguments: { where: {}, take: 10 },
349
+ },
350
+ }),
351
+ })
352
+
353
+ const response = await handlers.POST(request)
354
+ expect(response.status).toBe(200)
355
+
356
+ const data = await response.json()
357
+ expect(data.result.content[0].text).toContain('"occurredAtMs": "9007199254740993"')
358
+
359
+ const result = JSON.parse(data.result.content[0].text)
360
+ expect(result.items[0].occurredAtMs).toBe('9007199254740993')
361
+ })
362
+
329
363
  it('should execute create operation', async () => {
330
364
  const mockResult = { id: '1', title: 'New Post', content: 'New Content' }
331
365
  mockPrisma.post.create.mockResolvedValue(mockResult)