@opensaas/stack-core 0.20.1 → 0.22.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 (136) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +334 -0
  3. package/CLAUDE.md +29 -11
  4. package/dist/access/access-filter.d.ts +29 -0
  5. package/dist/access/access-filter.d.ts.map +1 -0
  6. package/dist/access/access-filter.js +68 -0
  7. package/dist/access/access-filter.js.map +1 -0
  8. package/dist/access/engine.d.ts +15 -48
  9. package/dist/access/engine.d.ts.map +1 -1
  10. package/dist/access/engine.js +14 -280
  11. package/dist/access/engine.js.map +1 -1
  12. package/dist/access/field-access.d.ts +44 -0
  13. package/dist/access/field-access.d.ts.map +1 -0
  14. package/dist/access/field-access.js +123 -0
  15. package/dist/access/field-access.js.map +1 -0
  16. package/dist/access/field-access.test.d.ts +2 -0
  17. package/dist/access/field-access.test.d.ts.map +1 -0
  18. package/dist/access/{engine.test.js → field-access.test.js} +2 -2
  19. package/dist/access/field-access.test.js.map +1 -0
  20. package/dist/access/field-visibility.d.ts +13 -0
  21. package/dist/access/field-visibility.d.ts.map +1 -0
  22. package/dist/access/field-visibility.js +178 -0
  23. package/dist/access/field-visibility.js.map +1 -0
  24. package/dist/access/index.d.ts +4 -1
  25. package/dist/access/index.d.ts.map +1 -1
  26. package/dist/access/index.js +8 -1
  27. package/dist/access/index.js.map +1 -1
  28. package/dist/access/multi-column-read-write.test.d.ts +2 -0
  29. package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
  30. package/dist/access/multi-column-read-write.test.js +149 -0
  31. package/dist/access/multi-column-read-write.test.js.map +1 -0
  32. package/dist/config/index.d.ts +1 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +334 -5
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/context/hook-pipeline.d.ts +49 -0
  37. package/dist/context/hook-pipeline.d.ts.map +1 -0
  38. package/dist/context/hook-pipeline.js +75 -0
  39. package/dist/context/hook-pipeline.js.map +1 -0
  40. package/dist/context/index.d.ts.map +1 -1
  41. package/dist/context/index.js +30 -462
  42. package/dist/context/index.js.map +1 -1
  43. package/dist/context/nested-operations.d.ts.map +1 -1
  44. package/dist/context/nested-operations.js +72 -68
  45. package/dist/context/nested-operations.js.map +1 -1
  46. package/dist/context/write-pipeline.d.ts +158 -0
  47. package/dist/context/write-pipeline.d.ts.map +1 -0
  48. package/dist/context/write-pipeline.js +306 -0
  49. package/dist/context/write-pipeline.js.map +1 -0
  50. package/dist/extend.d.ts +3 -0
  51. package/dist/extend.d.ts.map +1 -0
  52. package/dist/extend.js +10 -0
  53. package/dist/extend.js.map +1 -0
  54. package/dist/fields/format-prisma-default.d.ts +35 -0
  55. package/dist/fields/format-prisma-default.d.ts.map +1 -0
  56. package/dist/fields/format-prisma-default.js +52 -0
  57. package/dist/fields/format-prisma-default.js.map +1 -0
  58. package/dist/fields/format-prisma-default.test.d.ts +2 -0
  59. package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
  60. package/dist/fields/format-prisma-default.test.js +54 -0
  61. package/dist/fields/format-prisma-default.test.js.map +1 -0
  62. package/dist/fields/index.d.ts +1 -0
  63. package/dist/fields/index.d.ts.map +1 -1
  64. package/dist/fields/index.js +267 -18
  65. package/dist/fields/index.js.map +1 -1
  66. package/dist/fields/select.test.js +85 -0
  67. package/dist/fields/select.test.js.map +1 -1
  68. package/dist/fields/text-keystone-compat.test.d.ts +2 -0
  69. package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
  70. package/dist/fields/text-keystone-compat.test.js +93 -0
  71. package/dist/fields/text-keystone-compat.test.js.map +1 -0
  72. package/dist/hooks/index.d.ts +20 -0
  73. package/dist/hooks/index.d.ts.map +1 -1
  74. package/dist/hooks/index.js +246 -0
  75. package/dist/hooks/index.js.map +1 -1
  76. package/dist/index.d.ts +6 -8
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +25 -9
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.test.d.ts +2 -0
  81. package/dist/index.test.d.ts.map +1 -0
  82. package/dist/index.test.js +33 -0
  83. package/dist/index.test.js.map +1 -0
  84. package/dist/internal.d.ts +8 -0
  85. package/dist/internal.d.ts.map +1 -0
  86. package/dist/internal.js +16 -0
  87. package/dist/internal.js.map +1 -0
  88. package/dist/mcp/handler.js +0 -1
  89. package/dist/mcp/handler.js.map +1 -1
  90. package/dist/validation/field-config.d.ts +55 -0
  91. package/dist/validation/field-config.d.ts.map +1 -0
  92. package/dist/validation/field-config.js +100 -0
  93. package/dist/validation/field-config.js.map +1 -0
  94. package/dist/validation/field-config.test.d.ts +2 -0
  95. package/dist/validation/field-config.test.d.ts.map +1 -0
  96. package/dist/validation/field-config.test.js +159 -0
  97. package/dist/validation/field-config.test.js.map +1 -0
  98. package/package.json +11 -3
  99. package/src/access/access-filter.ts +97 -0
  100. package/src/access/engine.ts +13 -396
  101. package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
  102. package/src/access/field-access.ts +159 -0
  103. package/src/access/field-visibility.ts +269 -0
  104. package/src/access/index.ts +7 -4
  105. package/src/access/multi-column-read-write.test.ts +255 -0
  106. package/src/config/index.ts +3 -0
  107. package/src/config/types.ts +342 -4
  108. package/src/context/hook-pipeline.ts +160 -0
  109. package/src/context/index.ts +29 -667
  110. package/src/context/nested-operations.ts +142 -111
  111. package/src/context/write-pipeline.ts +543 -0
  112. package/src/extend.ts +19 -0
  113. package/src/fields/format-prisma-default.test.ts +64 -0
  114. package/src/fields/format-prisma-default.ts +67 -0
  115. package/src/fields/index.ts +375 -20
  116. package/src/fields/select.test.ts +99 -0
  117. package/src/fields/text-keystone-compat.test.ts +126 -0
  118. package/src/hooks/index.ts +270 -0
  119. package/src/index.test.ts +50 -0
  120. package/src/index.ts +35 -82
  121. package/src/internal.ts +49 -0
  122. package/src/mcp/handler.ts +0 -2
  123. package/src/validation/field-config.test.ts +199 -0
  124. package/src/validation/field-config.ts +145 -0
  125. package/tests/access-relationships.test.ts +4 -4
  126. package/tests/access.test.ts +1 -1
  127. package/tests/field-hooks.test.ts +410 -0
  128. package/tests/field-types.test.ts +1 -1
  129. package/tests/hook-pipeline.test.ts +233 -0
  130. package/tests/nested-operation-registry.test.ts +206 -0
  131. package/tests/write-pipeline.test.ts +588 -0
  132. package/tsconfig.tsbuildinfo +1 -1
  133. package/vitest.config.ts +43 -1
  134. package/dist/access/engine.test.d.ts +0 -2
  135. package/dist/access/engine.test.d.ts.map +0 -1
  136. package/dist/access/engine.test.js.map +0 -1
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { getContext } from '../src/context/index.js'
3
+ import { config, list } from '../src/config/index.js'
4
+ import { text, relationship } from '../src/fields/index.js'
5
+
6
+ /**
7
+ * These tests pin the behaviour of the nested-operation handler registry that
8
+ * sits behind `processNestedOperations`. Each nested-op kind (create, connect,
9
+ * connectOrCreate, update) plus the pass-through kinds (disconnect, delete,
10
+ * deleteMany, set, updateMany) is dispatched via the registry. The tests assert
11
+ * the exact payload handed to Prisma so a regression in dispatch/ordering is
12
+ * caught.
13
+ */
14
+
15
+ function createMockPrisma() {
16
+ return {
17
+ post: {
18
+ findFirst: vi.fn(),
19
+ findMany: vi.fn(),
20
+ findUnique: vi.fn(),
21
+ create: vi.fn(),
22
+ update: vi.fn(),
23
+ delete: vi.fn(),
24
+ count: vi.fn(),
25
+ },
26
+ user: {
27
+ findFirst: vi.fn(),
28
+ findMany: vi.fn(),
29
+ findUnique: vi.fn(),
30
+ create: vi.fn(),
31
+ update: vi.fn(),
32
+ delete: vi.fn(),
33
+ count: vi.fn(),
34
+ },
35
+ tag: {
36
+ findFirst: vi.fn(),
37
+ findMany: vi.fn(),
38
+ findUnique: vi.fn(),
39
+ create: vi.fn(),
40
+ update: vi.fn(),
41
+ delete: vi.fn(),
42
+ count: vi.fn(),
43
+ },
44
+ }
45
+ }
46
+
47
+ function buildConfig() {
48
+ return config({
49
+ db: {
50
+ provider: 'postgresql',
51
+ url: 'postgresql://localhost:5432/test',
52
+ },
53
+ lists: {
54
+ User: list({
55
+ fields: {
56
+ name: text(),
57
+ },
58
+ access: {
59
+ operation: {
60
+ query: () => true,
61
+ create: () => true,
62
+ update: () => true,
63
+ delete: () => true,
64
+ },
65
+ },
66
+ }),
67
+ Tag: list({
68
+ fields: {
69
+ label: text(),
70
+ },
71
+ access: {
72
+ operation: {
73
+ query: () => true,
74
+ create: () => true,
75
+ update: () => true,
76
+ delete: () => true,
77
+ },
78
+ },
79
+ }),
80
+ Post: list({
81
+ fields: {
82
+ title: text(),
83
+ author: relationship({ ref: 'User.posts' }),
84
+ tags: relationship({ ref: 'Tag', many: true }),
85
+ },
86
+ access: {
87
+ operation: {
88
+ query: () => true,
89
+ create: () => true,
90
+ update: () => true,
91
+ },
92
+ },
93
+ }),
94
+ },
95
+ })
96
+ }
97
+
98
+ describe('Nested Operation Handler Registry', () => {
99
+ let mockPrisma: ReturnType<typeof createMockPrisma>
100
+
101
+ beforeEach(() => {
102
+ mockPrisma = createMockPrisma()
103
+ vi.clearAllMocks()
104
+ mockPrisma.post.findUnique.mockResolvedValue({ id: '1', title: 'Original' })
105
+ mockPrisma.post.update.mockResolvedValue({ id: '1', title: 'Original' })
106
+ })
107
+
108
+ describe('pass-through kinds', () => {
109
+ it('passes disconnect through unchanged', async () => {
110
+ const context = getContext(await buildConfig(), mockPrisma, { userId: '1' })
111
+
112
+ await context.db.post.update({
113
+ where: { id: '1' },
114
+ data: { author: { disconnect: true } },
115
+ })
116
+
117
+ const passedData = mockPrisma.post.update.mock.calls[0][0].data
118
+ expect(passedData.author).toEqual({ disconnect: true })
119
+ })
120
+
121
+ it('passes delete, deleteMany, set and updateMany through unchanged', async () => {
122
+ const context = getContext(await buildConfig(), mockPrisma, { userId: '1' })
123
+
124
+ await context.db.post.update({
125
+ where: { id: '1' },
126
+ data: {
127
+ tags: {
128
+ delete: { id: 'a' },
129
+ deleteMany: { label: { contains: 'x' } },
130
+ set: [{ id: 'b' }],
131
+ updateMany: { where: { id: 'c' }, data: { label: 'renamed' } },
132
+ },
133
+ },
134
+ })
135
+
136
+ const passedTags = mockPrisma.post.update.mock.calls[0][0].data.tags
137
+ expect(passedTags).toEqual({
138
+ delete: { id: 'a' },
139
+ deleteMany: { label: { contains: 'x' } },
140
+ set: [{ id: 'b' }],
141
+ updateMany: { where: { id: 'c' }, data: { label: 'renamed' } },
142
+ })
143
+ })
144
+ })
145
+
146
+ describe('multiple kinds on a single field', () => {
147
+ it('dispatches create and disconnect together, preserving both', async () => {
148
+ const context = getContext(await buildConfig(), mockPrisma, { userId: '1' })
149
+
150
+ await context.db.post.update({
151
+ where: { id: '1' },
152
+ data: {
153
+ tags: {
154
+ create: { label: 'new-tag' },
155
+ disconnect: { id: 'old-tag' },
156
+ },
157
+ },
158
+ })
159
+
160
+ const passedTags = mockPrisma.post.update.mock.calls[0][0].data.tags
161
+ // create is processed through hooks/access (object preserved)
162
+ expect(passedTags.create).toEqual({ label: 'new-tag' })
163
+ // disconnect is passed through untouched
164
+ expect(passedTags.disconnect).toEqual({ id: 'old-tag' })
165
+ })
166
+ })
167
+
168
+ describe('connectOrCreate kind', () => {
169
+ it('produces a { where, create } payload via the registry', async () => {
170
+ mockPrisma.user.findUnique.mockResolvedValue(null)
171
+ const context = getContext(await buildConfig(), mockPrisma, { userId: '1' })
172
+
173
+ await context.db.post.update({
174
+ where: { id: '1' },
175
+ data: {
176
+ author: {
177
+ connectOrCreate: {
178
+ where: { id: '99' },
179
+ create: { name: 'Created Author' },
180
+ },
181
+ },
182
+ },
183
+ })
184
+
185
+ const passedAuthor = mockPrisma.post.update.mock.calls[0][0].data.author
186
+ expect(passedAuthor.connectOrCreate).toEqual({
187
+ where: { id: '99' },
188
+ create: { name: 'Created Author' },
189
+ })
190
+ })
191
+ })
192
+
193
+ describe('non-relationship fields', () => {
194
+ it('leaves scalar field values untouched', async () => {
195
+ const context = getContext(await buildConfig(), mockPrisma, { userId: '1' })
196
+
197
+ await context.db.post.update({
198
+ where: { id: '1' },
199
+ data: { title: 'Updated Title' },
200
+ })
201
+
202
+ const passedData = mockPrisma.post.update.mock.calls[0][0].data
203
+ expect(passedData.title).toBe('Updated Title')
204
+ })
205
+ })
206
+ })