@opensaas/stack-core 0.20.0 → 0.21.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 +74 -0
- package/CLAUDE.md +18 -2
- package/dist/access/access-filter.d.ts +29 -0
- package/dist/access/access-filter.d.ts.map +1 -0
- package/dist/access/access-filter.js +68 -0
- package/dist/access/access-filter.js.map +1 -0
- package/dist/access/engine.d.ts +15 -48
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +14 -280
- package/dist/access/engine.js.map +1 -1
- package/dist/access/field-access.d.ts +44 -0
- package/dist/access/field-access.d.ts.map +1 -0
- package/dist/access/field-access.js +123 -0
- package/dist/access/field-access.js.map +1 -0
- package/dist/access/field-access.test.d.ts +2 -0
- package/dist/access/field-access.test.d.ts.map +1 -0
- package/dist/access/{engine.test.js → field-access.test.js} +2 -2
- package/dist/access/field-access.test.js.map +1 -0
- package/dist/access/field-visibility.d.ts +13 -0
- package/dist/access/field-visibility.d.ts.map +1 -0
- package/dist/access/field-visibility.js +155 -0
- package/dist/access/field-visibility.js.map +1 -0
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +8 -1
- package/dist/access/index.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +45 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts +49 -0
- package/dist/context/hook-pipeline.d.ts.map +1 -0
- package/dist/context/hook-pipeline.js +75 -0
- package/dist/context/hook-pipeline.js.map +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +30 -462
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +72 -68
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +158 -0
- package/dist/context/write-pipeline.d.ts.map +1 -0
- package/dist/context/write-pipeline.js +306 -0
- package/dist/context/write-pipeline.js.map +1 -0
- package/dist/extend.d.ts +3 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +10 -0
- package/dist/extend.js.map +1 -0
- package/dist/fields/index.d.ts +1 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +213 -2
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +202 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -10
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +8 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +16 -0
- package/dist/internal.js.map +1 -0
- package/dist/validation/field-config.d.ts +55 -0
- package/dist/validation/field-config.d.ts.map +1 -0
- package/dist/validation/field-config.js +100 -0
- package/dist/validation/field-config.js.map +1 -0
- package/dist/validation/field-config.test.d.ts +2 -0
- package/dist/validation/field-config.test.d.ts.map +1 -0
- package/dist/validation/field-config.test.js +159 -0
- package/dist/validation/field-config.test.js.map +1 -0
- package/package.json +12 -4
- package/src/access/access-filter.ts +97 -0
- package/src/access/engine.ts +13 -396
- package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
- package/src/access/field-access.ts +159 -0
- package/src/access/field-visibility.ts +247 -0
- package/src/access/index.ts +7 -4
- package/src/config/index.ts +1 -0
- package/src/config/types.ts +51 -4
- package/src/context/hook-pipeline.ts +160 -0
- package/src/context/index.ts +29 -667
- package/src/context/nested-operations.ts +142 -111
- package/src/context/write-pipeline.ts +543 -0
- package/src/extend.ts +14 -0
- package/src/fields/index.ts +310 -2
- package/src/hooks/index.ts +227 -0
- package/src/index.ts +27 -90
- package/src/internal.ts +49 -0
- package/src/validation/field-config.test.ts +199 -0
- package/src/validation/field-config.ts +145 -0
- package/tests/access-relationships.test.ts +4 -4
- package/tests/access.test.ts +1 -1
- package/tests/field-hooks.test.ts +410 -0
- package/tests/field-types.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +233 -0
- package/tests/nested-operation-registry.test.ts +206 -0
- package/tests/write-pipeline.test.ts +588 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +43 -1
- package/dist/access/engine.test.d.ts +0 -2
- package/dist/access/engine.test.d.ts.map +0 -1
- 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
|
+
})
|