@selvajs/platform 0.14.3-beta.0 → 0.15.0-beta.2

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 (154) hide show
  1. package/LICENSE +1 -1
  2. package/dist/computeServer/index.d.ts +1 -1
  3. package/dist/computeServer/index.d.ts.map +1 -1
  4. package/dist/computeServer/interface.d.ts +29 -1
  5. package/dist/computeServer/interface.d.ts.map +1 -1
  6. package/dist/computeServer/types.d.ts +12 -1
  7. package/dist/computeServer/types.d.ts.map +1 -1
  8. package/dist/computeServer/types.js.map +1 -1
  9. package/dist/data/index.d.ts +2 -1
  10. package/dist/data/index.d.ts.map +1 -1
  11. package/dist/data/index.js +1 -1
  12. package/dist/data/index.js.map +1 -1
  13. package/dist/data/interface.d.ts +69 -8
  14. package/dist/data/interface.d.ts.map +1 -1
  15. package/dist/data/interface.js +7 -1
  16. package/dist/data/interface.js.map +1 -1
  17. package/dist/definitions/interface.d.ts +16 -1
  18. package/dist/definitions/interface.d.ts.map +1 -1
  19. package/dist/definitions/schemas.d.ts +1 -0
  20. package/dist/definitions/schemas.d.ts.map +1 -1
  21. package/dist/definitions/schemas.js +8 -0
  22. package/dist/definitions/schemas.js.map +1 -1
  23. package/dist/definitions/types.d.ts +24 -1
  24. package/dist/definitions/types.d.ts.map +1 -1
  25. package/dist/events/index.d.ts +1 -1
  26. package/dist/events/index.d.ts.map +1 -1
  27. package/dist/events/index.js +1 -1
  28. package/dist/events/index.js.map +1 -1
  29. package/dist/events/interface.d.ts +30 -0
  30. package/dist/events/interface.d.ts.map +1 -1
  31. package/dist/events/interface.js +7 -0
  32. package/dist/events/interface.js.map +1 -1
  33. package/dist/index.d.ts +9 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/logging/interface.d.ts +82 -0
  38. package/dist/logging/interface.d.ts.map +1 -0
  39. package/dist/logging/interface.js +33 -0
  40. package/dist/logging/interface.js.map +1 -0
  41. package/dist/metrics/interface.d.ts +6 -0
  42. package/dist/metrics/interface.d.ts.map +1 -1
  43. package/dist/metrics/interface.js.map +1 -1
  44. package/dist/organizations/__tests__/slug-schema.test.d.ts +2 -0
  45. package/dist/organizations/__tests__/slug-schema.test.d.ts.map +1 -0
  46. package/dist/organizations/__tests__/slug-schema.test.js +38 -0
  47. package/dist/organizations/__tests__/slug-schema.test.js.map +1 -0
  48. package/dist/organizations/index.d.ts +1 -1
  49. package/dist/organizations/index.d.ts.map +1 -1
  50. package/dist/organizations/index.js +1 -1
  51. package/dist/organizations/index.js.map +1 -1
  52. package/dist/organizations/schemas.d.ts +16 -2
  53. package/dist/organizations/schemas.d.ts.map +1 -1
  54. package/dist/organizations/schemas.js +32 -2
  55. package/dist/organizations/schemas.js.map +1 -1
  56. package/dist/solveCache/interface.d.ts +86 -0
  57. package/dist/solveCache/interface.d.ts.map +1 -0
  58. package/dist/solveCache/interface.js +12 -0
  59. package/dist/solveCache/interface.js.map +1 -0
  60. package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
  61. package/dist/testing/suites/definitionStoreSuite.js +57 -0
  62. package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
  63. package/dist/testing/suites/eventSinkSuite.js +1 -0
  64. package/dist/testing/suites/eventSinkSuite.js.map +1 -1
  65. package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
  66. package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
  67. package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
  68. package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
  69. package/dist/testing/suites/solveMetricSinkSuite.d.ts +5 -0
  70. package/dist/testing/suites/solveMetricSinkSuite.d.ts.map +1 -1
  71. package/dist/testing/suites/solveMetricSinkSuite.js +17 -8
  72. package/dist/testing/suites/solveMetricSinkSuite.js.map +1 -1
  73. package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
  74. package/dist/testing/suites/storageProviderSuite.js +23 -2
  75. package/dist/testing/suites/storageProviderSuite.js.map +1 -1
  76. package/package.json +9 -24
  77. package/src/access/index.ts +0 -23
  78. package/src/access/rules.ts +0 -276
  79. package/src/auth/index.ts +0 -8
  80. package/src/auth/interface.ts +0 -279
  81. package/src/auth/types.ts +0 -32
  82. package/src/bindings/index.ts +0 -2
  83. package/src/bindings/interface.ts +0 -61
  84. package/src/computeServer/index.ts +0 -28
  85. package/src/computeServer/interface.ts +0 -73
  86. package/src/computeServer/secrets.ts +0 -102
  87. package/src/computeServer/types.ts +0 -74
  88. package/src/computeServer/utils.ts +0 -112
  89. package/src/config.ts +0 -99
  90. package/src/context.ts +0 -80
  91. package/src/data/index.ts +0 -1
  92. package/src/data/interface.ts +0 -70
  93. package/src/definitions/index.ts +0 -26
  94. package/src/definitions/interface.ts +0 -87
  95. package/src/definitions/paths.ts +0 -31
  96. package/src/definitions/schemas.ts +0 -64
  97. package/src/definitions/types.ts +0 -111
  98. package/src/errors/interface.ts +0 -45
  99. package/src/errors.ts +0 -16
  100. package/src/events/audit.ts +0 -67
  101. package/src/events/index.ts +0 -9
  102. package/src/events/interface.ts +0 -102
  103. package/src/index.ts +0 -247
  104. package/src/invites/index.ts +0 -2
  105. package/src/invites/interface.ts +0 -38
  106. package/src/invites/types.ts +0 -39
  107. package/src/metrics/interface.ts +0 -86
  108. package/src/organizations/index.ts +0 -23
  109. package/src/organizations/interface.ts +0 -68
  110. package/src/organizations/paths.ts +0 -45
  111. package/src/organizations/schemas.ts +0 -61
  112. package/src/organizations/types.ts +0 -41
  113. package/src/pagination.ts +0 -45
  114. package/src/permissions/index.ts +0 -3
  115. package/src/permissions/interface.ts +0 -60
  116. package/src/permissions/types.ts +0 -21
  117. package/src/platformProjects/index.ts +0 -3
  118. package/src/platformProjects/interface.ts +0 -27
  119. package/src/platformProjects/schemas.ts +0 -13
  120. package/src/platformProjects/types.ts +0 -22
  121. package/src/projects/index.ts +0 -4
  122. package/src/projects/interface.ts +0 -58
  123. package/src/projects/schemas.ts +0 -28
  124. package/src/projects/types.ts +0 -36
  125. package/src/shareLinks/index.ts +0 -5
  126. package/src/shareLinks/interface.ts +0 -35
  127. package/src/shareLinks/schemas.ts +0 -21
  128. package/src/shareLinks/types.ts +0 -34
  129. package/src/storage/assetClasses.ts +0 -134
  130. package/src/storage/cacheBust.ts +0 -26
  131. package/src/storage/image.ts +0 -82
  132. package/src/storage/index.ts +0 -12
  133. package/src/storage/interface.ts +0 -27
  134. package/src/testing/index.ts +0 -47
  135. package/src/testing/suites/authProviderSuite.ts +0 -207
  136. package/src/testing/suites/computeServerStoreSuite.ts +0 -195
  137. package/src/testing/suites/definitionStoreSuite.ts +0 -630
  138. package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
  139. package/src/testing/suites/eventSinkSuite.ts +0 -379
  140. package/src/testing/suites/helpers.ts +0 -87
  141. package/src/testing/suites/inviteStoreSuite.ts +0 -143
  142. package/src/testing/suites/orgStoreSuite.ts +0 -429
  143. package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
  144. package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
  145. package/src/testing/suites/projectStoreSuite.ts +0 -381
  146. package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
  147. package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
  148. package/src/testing/suites/storageProviderSuite.ts +0 -196
  149. package/src/testing/suites/userProfileStoreSuite.ts +0 -134
  150. package/src/userProfile/index.ts +0 -3
  151. package/src/userProfile/interface.ts +0 -48
  152. package/src/userProfile/types.ts +0 -24
  153. package/src/utils/audit.ts +0 -46
  154. package/src/utils/index.ts +0 -2
@@ -1,630 +0,0 @@
1
- /**
2
- * Adapter conformance suite for IDefinitionStore.
3
- *
4
- * The goal is to prove every adapter behaves identically from the consuming
5
- * app's perspective — `ctx` scoping, `pending` filtering, version CRUD +
6
- * reference protection (Permissions spec §6), and error shapes.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest';
10
- import type { IDefinitionStore } from '../../definitions/interface.js';
11
- import type { DefinitionRecord, DefinitionVersion } from '../../definitions/types.js';
12
- import { makeCtx, makeUuid } from './helpers.js';
13
- import type { RequestContext } from '../../context.js';
14
-
15
- /**
16
- * Scope the suite needs to reference from each test. Adapters that enforce
17
- * FK constraints (Supabase) return real uuids they've seeded; adapters that
18
- * don't (local JSON) can omit this and the suite uses the defaults.
19
- */
20
- export interface DefinitionTestScope {
21
- /** Primary owner. Every record and ctx in the suite uses this id. */
22
- ownerId: string;
23
- /**
24
- * Optional access token for `ownerId`. Required for adapters that enforce
25
- * auth at the DB layer (Supabase RLS); ignored by local providers.
26
- */
27
- ownerSessionToken?: string;
28
- /** Default project id. Returned by `primaryProjectId()` in the suite. */
29
- projectId: string;
30
- /**
31
- * Second project id used by `listByProject` tests to prove filtering.
32
- * Must reference a real row for FK-enforcing adapters.
33
- */
34
- secondaryProjectId: string;
35
- /** Optional third user id for ctx-isolation tests. Required when ctxIsolation is set. */
36
- secondaryUserId?: string;
37
- /** Token for `secondaryUserId`, when used. */
38
- secondaryUserSessionToken?: string;
39
- }
40
-
41
- export interface DefinitionStoreConformanceOptions {
42
- /** Name to show in the test output (e.g. "local-provider"). */
43
- name: string;
44
- /** Factory that returns a fresh, empty store per test. */
45
- createStore: () => Promise<IDefinitionStore> | IDefinitionStore;
46
- /**
47
- * Per-test scope factory. Called once in each `it` block. Adapters with FK
48
- * constraints seed the necessary `auth.users` / `projects` rows here and
49
- * return the generated ids; adapters without FKs can return stub ids.
50
- */
51
- createScope?: () => Promise<DefinitionTestScope> | DefinitionTestScope;
52
- /**
53
- * Set to true for adapters that enforce per-user ctx scoping (e.g. Supabase RLS).
54
- * When false, ctx-isolation tests are skipped (local JSON adapter shares all records).
55
- */
56
- ctxIsolation?: boolean;
57
- }
58
-
59
- const DEFAULT_SCOPE: DefinitionTestScope = {
60
- ownerId: 'user-1',
61
- projectId: 'project-1',
62
- secondaryProjectId: 'project-2',
63
- secondaryUserId: 'user-b'
64
- };
65
-
66
- // Suite-wide token map. `scopeFor` populates it from the scope; `ctx`
67
- // auto-attaches the matching token so adapters with DB-side auth (Supabase
68
- // RLS) run as the right user. Local providers ignore tokens entirely.
69
- const tokens = new Map<string, string>();
70
-
71
- function ctx(
72
- userId: string,
73
- opts: {
74
- actingOrgId?: string;
75
- platformPermissions?: RequestContext['platformPermissions'];
76
- orgPermissions?: RequestContext['orgPermissions'];
77
- } = {}
78
- ): RequestContext {
79
- return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
80
- }
81
-
82
- function recordTokensFromScope(scope: DefinitionTestScope): void {
83
- if (scope.ownerSessionToken) tokens.set(scope.ownerId, scope.ownerSessionToken);
84
- if (scope.secondaryUserId && scope.secondaryUserSessionToken) {
85
- tokens.set(scope.secondaryUserId, scope.secondaryUserSessionToken);
86
- }
87
- }
88
-
89
- function record(
90
- scope: DefinitionTestScope,
91
- overrides: Partial<DefinitionRecord> = {}
92
- ): DefinitionRecord {
93
- const now = new Date().toISOString();
94
- return {
95
- guid: overrides.guid ?? makeUuid(),
96
- projectId: overrides.projectId ?? scope.projectId,
97
- ownerId: overrides.ownerId ?? scope.ownerId,
98
- createdBy: overrides.createdBy ?? scope.ownerId,
99
- updatedBy: overrides.updatedBy ?? scope.ownerId,
100
- displayName: overrides.displayName ?? 'Test',
101
- status: overrides.status ?? 'published',
102
- solveCount: overrides.solveCount ?? 0,
103
- liveVersionId: overrides.liveVersionId ?? null,
104
- draftVersionId: overrides.draftVersionId ?? null,
105
- createdAt: overrides.createdAt ?? now,
106
- updatedAt: overrides.updatedAt ?? now,
107
- deletedAt: overrides.deletedAt ?? null,
108
- ...overrides
109
- };
110
- }
111
-
112
- function version(
113
- definitionId: string,
114
- versionNumber: number,
115
- uploadedBy: string,
116
- overrides: Partial<DefinitionVersion> = {}
117
- ): DefinitionVersion {
118
- return {
119
- id: overrides.id ?? makeUuid(),
120
- definitionId,
121
- versionNumber,
122
- fileExt: overrides.fileExt ?? 'gh',
123
- fileKey: overrides.fileKey ?? `definitions/${definitionId}/versions/v${versionNumber}.gh`,
124
- originalFilename: overrides.originalFilename,
125
- uploadedBy,
126
- uploadedAt: overrides.uploadedAt ?? new Date().toISOString(),
127
- ...overrides
128
- };
129
- }
130
-
131
- export function runDefinitionStoreConformance(opts: DefinitionStoreConformanceOptions): void {
132
- const { name, createStore, createScope, ctxIsolation = false } = opts;
133
- const scopeFor = async (): Promise<DefinitionTestScope> => {
134
- const s = createScope ? await createScope() : DEFAULT_SCOPE;
135
- recordTokensFromScope(s);
136
- return s;
137
- };
138
-
139
- describe(`IDefinitionStore conformance: ${name}`, () => {
140
- it('create + get returns the record', async () => {
141
- const store = await createStore();
142
- const scope = await scopeFor();
143
- const guid = makeUuid();
144
- const rec = record(scope, { guid });
145
- await store.create(ctx(scope.ownerId), rec);
146
- const got = await store.get(ctx(scope.ownerId), guid);
147
- expect(got?.guid).toBe(guid);
148
- });
149
-
150
- it('get returns null for missing guid', async () => {
151
- const store = await createStore();
152
- const scope = await scopeFor();
153
- const got = await store.get(ctx(scope.ownerId), makeUuid());
154
- expect(got).toBeNull();
155
- });
156
-
157
- it('list filters pending by default', async () => {
158
- const store = await createStore();
159
- const scope = await scopeFor();
160
- const pub = makeUuid();
161
- const pend = makeUuid();
162
- await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
163
- await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
164
-
165
- const page = await store.list(ctx(scope.ownerId));
166
- const guids = page.items.map((r) => r.guid);
167
- expect(guids).toContain(pub);
168
- expect(guids).not.toContain(pend);
169
- });
170
-
171
- it('list with includePending returns pending too', async () => {
172
- const store = await createStore();
173
- const scope = await scopeFor();
174
- const pub = makeUuid();
175
- const pend = makeUuid();
176
- await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
177
- await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
178
-
179
- const page = await store.list(ctx(scope.ownerId), { includePending: true });
180
- const guids = page.items.map((r) => r.guid);
181
- expect(guids).toContain(pub);
182
- expect(guids).toContain(pend);
183
- });
184
-
185
- it('list with statuses filter returns only matching statuses', async () => {
186
- const store = await createStore();
187
- const scope = await scopeFor();
188
- const pub = makeUuid();
189
- const dft = makeUuid();
190
- await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
191
- await store.create(ctx(scope.ownerId), record(scope, { guid: dft, status: 'draft' }));
192
-
193
- const page = await store.list(ctx(scope.ownerId), { statuses: ['draft'] });
194
- const guids = page.items.map((r) => r.guid);
195
- expect(guids).toContain(dft);
196
- expect(guids).not.toContain(pub);
197
- });
198
-
199
- it('listByProject filters by projectId', async () => {
200
- const store = await createStore();
201
- const scope = await scopeFor();
202
- const a = makeUuid();
203
- const b = makeUuid();
204
- await store.create(
205
- ctx(scope.ownerId),
206
- record(scope, { guid: a, projectId: scope.projectId })
207
- );
208
- await store.create(
209
- ctx(scope.ownerId),
210
- record(scope, { guid: b, projectId: scope.secondaryProjectId })
211
- );
212
-
213
- const page = await store.listByProject(ctx(scope.ownerId), scope.projectId);
214
- expect(page.items.map((r) => r.guid)).toEqual([a]);
215
- });
216
-
217
- it('update applies patch and bumps updatedAt', async () => {
218
- const store = await createStore();
219
- const scope = await scopeFor();
220
- const guid = makeUuid();
221
- const created = record(scope, { guid, displayName: 'Old' });
222
- await store.create(ctx(scope.ownerId), created);
223
-
224
- await store.update(ctx(scope.ownerId), guid, { displayName: 'New' });
225
- const got = await store.get(ctx(scope.ownerId), guid);
226
- expect(got?.displayName).toBe('New');
227
- expect(got!.updatedAt >= created.updatedAt).toBe(true);
228
- });
229
-
230
- it('update on missing guid throws ProviderError with status 404', async () => {
231
- const store = await createStore();
232
- const scope = await scopeFor();
233
- let thrown: unknown;
234
- try {
235
- await store.update(ctx(scope.ownerId), makeUuid(), { displayName: 'X' });
236
- } catch (err) {
237
- thrown = err;
238
- }
239
- expect(thrown).toBeTruthy();
240
- const e = thrown as { statusCode?: number };
241
- expect(e.statusCode).toBe(404);
242
- });
243
-
244
- it('status flip pending → published via update', async () => {
245
- const store = await createStore();
246
- const scope = await scopeFor();
247
- const guid = makeUuid();
248
- await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'pending' }));
249
-
250
- const beforeFlip = await store.list(ctx(scope.ownerId));
251
- expect(beforeFlip.items.map((r) => r.guid)).not.toContain(guid);
252
-
253
- await store.update(ctx(scope.ownerId), guid, { status: 'published' });
254
- const afterFlip = await store.list(ctx(scope.ownerId));
255
- expect(afterFlip.items.map((r) => r.guid)).toContain(guid);
256
- });
257
-
258
- it('incrementSolveCount increases solveCount by 1', async () => {
259
- const store = await createStore();
260
- const scope = await scopeFor();
261
- const guid = makeUuid();
262
- await store.create(ctx(scope.ownerId), record(scope, { guid, solveCount: 5 }));
263
- await store.incrementSolveCount(ctx(scope.ownerId), guid);
264
- const got = await store.get(ctx(scope.ownerId), guid);
265
- expect(got?.solveCount).toBe(6);
266
- });
267
-
268
- it('incrementSolveCount is a no-op for missing guid', async () => {
269
- const store = await createStore();
270
- const scope = await scopeFor();
271
- let threw = false;
272
- try {
273
- await store.incrementSolveCount(ctx(scope.ownerId), makeUuid());
274
- } catch {
275
- threw = true;
276
- }
277
- expect(threw).toBe(false);
278
- });
279
-
280
- it('update advances updatedBy to the caller', async () => {
281
- const store = await createStore();
282
- const scope = await scopeFor();
283
- const guid = makeUuid();
284
- const editor = scope.secondaryUserId ?? makeUuid();
285
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
286
- // Editing as a different user — updatedBy should advance.
287
- await store.update(ctx(editor), guid, { displayName: 'Renamed' });
288
- const got = await store.get(ctx(scope.ownerId), guid);
289
- expect(got?.updatedBy).toBe(editor);
290
- expect(got?.createdBy).toBe(scope.ownerId);
291
- });
292
-
293
- // ============================================================================
294
- // Versions (Permissions spec §6)
295
- // ============================================================================
296
-
297
- it('createVersion + listVersions: rows return newest-first by versionNumber', async () => {
298
- const store = await createStore();
299
- const scope = await scopeFor();
300
- const guid = makeUuid();
301
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
302
-
303
- const v1 = version(guid, 1, scope.ownerId);
304
- const v2 = version(guid, 2, scope.ownerId);
305
- await store.createVersion(ctx(scope.ownerId), v1);
306
- await store.createVersion(ctx(scope.ownerId), v2);
307
-
308
- const page = await store.listVersions(ctx(scope.ownerId), guid);
309
- expect(page.items.map((v) => v.versionNumber)).toEqual([2, 1]);
310
- expect(page.items.map((v) => v.id)).toEqual([v2.id, v1.id]);
311
- });
312
-
313
- it('getVersion returns the row by id', async () => {
314
- const store = await createStore();
315
- const scope = await scopeFor();
316
- const guid = makeUuid();
317
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
318
- const v1 = version(guid, 1, scope.ownerId);
319
- await store.createVersion(ctx(scope.ownerId), v1);
320
-
321
- const got = await store.getVersion(ctx(scope.ownerId), v1.id);
322
- expect(got?.id).toBe(v1.id);
323
- expect(got?.versionNumber).toBe(1);
324
- });
325
-
326
- it('setVersionSchema persists a cached schema on the version', async () => {
327
- const store = await createStore();
328
- const scope = await scopeFor();
329
- const guid = makeUuid();
330
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
331
- const v1 = version(guid, 1, scope.ownerId);
332
- await store.createVersion(ctx(scope.ownerId), v1);
333
- expect((await store.getVersion(ctx(scope.ownerId), v1.id))?.schema).toBeUndefined();
334
-
335
- const schema = {
336
- name: 'Test',
337
- inputs: [],
338
- outputs: []
339
- } as unknown as Parameters<typeof store.setVersionSchema>[2];
340
- await store.setVersionSchema(ctx(scope.ownerId), v1.id, schema);
341
-
342
- const got = await store.getVersion(ctx(scope.ownerId), v1.id);
343
- expect(got?.schema).toEqual(schema);
344
- expect(got?.schemaExtractedAt).toBeTypeOf('string');
345
- });
346
-
347
- it('setVersionSchema is a no-op for a missing version', async () => {
348
- const store = await createStore();
349
- const scope = await scopeFor();
350
- const schema = { name: 'X', inputs: [], outputs: [] } as unknown as Parameters<
351
- typeof store.setVersionSchema
352
- >[2];
353
- await expect(
354
- store.setVersionSchema(ctx(scope.ownerId), makeUuid(), schema)
355
- ).resolves.toBeUndefined();
356
- });
357
-
358
- it('setLiveVersion + setDraftVersion repoint channels', async () => {
359
- const store = await createStore();
360
- const scope = await scopeFor();
361
- const guid = makeUuid();
362
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
363
- const v1 = version(guid, 1, scope.ownerId);
364
- const v2 = version(guid, 2, scope.ownerId);
365
- await store.createVersion(ctx(scope.ownerId), v1);
366
- await store.createVersion(ctx(scope.ownerId), v2);
367
-
368
- await store.setDraftVersion(ctx(scope.ownerId), guid, v2.id);
369
- let got = await store.get(ctx(scope.ownerId), guid);
370
- expect(got?.draftVersionId).toBe(v2.id);
371
- expect(got?.liveVersionId).toBeNull();
372
-
373
- await store.setLiveVersion(ctx(scope.ownerId), guid, v1.id);
374
- got = await store.get(ctx(scope.ownerId), guid);
375
- expect(got?.liveVersionId).toBe(v1.id);
376
- expect(got?.draftVersionId).toBe(v2.id);
377
- });
378
-
379
- it('attachInitialVersion: bootstraps both pointers + status atomically', async () => {
380
- const store = await createStore();
381
- const scope = await scopeFor();
382
- const guid = makeUuid();
383
- // Mirrors what DefinitionService.create writes: status='pending',
384
- // no pointers — the state attachInitialVersion is meant to advance.
385
- await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'pending' }));
386
- const v1 = version(guid, 1, scope.ownerId);
387
- await store.createVersion(ctx(scope.ownerId), v1);
388
-
389
- await store.attachInitialVersion(ctx(scope.ownerId), guid, v1.id);
390
-
391
- const got = await store.get(ctx(scope.ownerId), guid);
392
- expect(got?.status).toBe('draft');
393
- expect(got?.liveVersionId).toBe(v1.id);
394
- expect(got?.draftVersionId).toBe(v1.id);
395
- });
396
-
397
- it('attachInitialVersion: rejects a version belonging to another definition', async () => {
398
- const store = await createStore();
399
- const scope = await scopeFor();
400
- const guidA = makeUuid();
401
- const guidB = makeUuid();
402
- await store.create(ctx(scope.ownerId), record(scope, { guid: guidA, status: 'pending' }));
403
- await store.create(ctx(scope.ownerId), record(scope, { guid: guidB, status: 'pending' }));
404
- const vForB = version(guidB, 1, scope.ownerId);
405
- await store.createVersion(ctx(scope.ownerId), vForB);
406
-
407
- let thrown: unknown;
408
- try {
409
- await store.attachInitialVersion(ctx(scope.ownerId), guidA, vForB.id);
410
- } catch (err) {
411
- thrown = err;
412
- }
413
- expect((thrown as { statusCode?: number })?.statusCode).toBe(404);
414
-
415
- // And A's state must still be the bootstrap-pending shape.
416
- const got = await store.get(ctx(scope.ownerId), guidA);
417
- expect(got?.status).toBe('pending');
418
- expect(got?.liveVersionId).toBeNull();
419
- expect(got?.draftVersionId).toBeNull();
420
- });
421
-
422
- it('setLiveVersion rejects a version belonging to another definition', async () => {
423
- const store = await createStore();
424
- const scope = await scopeFor();
425
- const guidA = makeUuid();
426
- const guidB = makeUuid();
427
- await store.create(ctx(scope.ownerId), record(scope, { guid: guidA }));
428
- await store.create(ctx(scope.ownerId), record(scope, { guid: guidB }));
429
- const vForB = version(guidB, 1, scope.ownerId);
430
- await store.createVersion(ctx(scope.ownerId), vForB);
431
-
432
- let thrown: unknown;
433
- try {
434
- await store.setLiveVersion(ctx(scope.ownerId), guidA, vForB.id);
435
- } catch (err) {
436
- thrown = err;
437
- }
438
- expect((thrown as { statusCode?: number })?.statusCode).toBe(404);
439
- });
440
-
441
- it('deleteVersion: succeeds when not referenced by live or draft', async () => {
442
- const store = await createStore();
443
- const scope = await scopeFor();
444
- const guid = makeUuid();
445
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
446
- const v1 = version(guid, 1, scope.ownerId);
447
- const v2 = version(guid, 2, scope.ownerId);
448
- await store.createVersion(ctx(scope.ownerId), v1);
449
- await store.createVersion(ctx(scope.ownerId), v2);
450
- await store.setLiveVersion(ctx(scope.ownerId), guid, v2.id);
451
- await store.setDraftVersion(ctx(scope.ownerId), guid, v2.id);
452
-
453
- await store.deleteVersion(ctx(scope.ownerId), v1.id);
454
- const remaining = await store.listVersions(ctx(scope.ownerId), guid);
455
- expect(remaining.items.map((v) => v.id)).toEqual([v2.id]);
456
- });
457
-
458
- it('deleteVersion: throws 409 when the version is referenced by liveVersionId', async () => {
459
- const store = await createStore();
460
- const scope = await scopeFor();
461
- const guid = makeUuid();
462
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
463
- const v1 = version(guid, 1, scope.ownerId);
464
- await store.createVersion(ctx(scope.ownerId), v1);
465
- await store.setLiveVersion(ctx(scope.ownerId), guid, v1.id);
466
-
467
- let thrown: unknown;
468
- try {
469
- await store.deleteVersion(ctx(scope.ownerId), v1.id);
470
- } catch (err) {
471
- thrown = err;
472
- }
473
- expect((thrown as { statusCode?: number })?.statusCode).toBe(409);
474
- });
475
-
476
- it('delete removes the record', async () => {
477
- const store = await createStore();
478
- const scope = await scopeFor();
479
- const guid = makeUuid();
480
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
481
- await store.delete(ctx(scope.ownerId), guid);
482
- const got = await store.get(ctx(scope.ownerId), guid);
483
- expect(got).toBeNull();
484
- });
485
-
486
- // ============================================================================
487
- // Audit fields + soft delete
488
- // ============================================================================
489
-
490
- it('create populates createdBy/updatedBy and deletedAt=null', async () => {
491
- const store = await createStore();
492
- const scope = await scopeFor();
493
- const guid = makeUuid();
494
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
495
- const got = await store.get(ctx(scope.ownerId), guid);
496
- expect(got?.createdBy).toBe(scope.ownerId);
497
- expect(got?.updatedBy).toBe(scope.ownerId);
498
- expect(got?.deletedAt ?? null).toBeNull();
499
- });
500
-
501
- it('versioning scaffold: liveVersionId/draftVersionId default to null on create', async () => {
502
- const store = await createStore();
503
- const scope = await scopeFor();
504
- const guid = makeUuid();
505
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
506
- const got = await store.get(ctx(scope.ownerId), guid);
507
- expect(got?.liveVersionId ?? null).toBeNull();
508
- expect(got?.draftVersionId ?? null).toBeNull();
509
- });
510
-
511
- it('delete soft-deletes — record excluded from list/listByProject', async () => {
512
- const store = await createStore();
513
- const scope = await scopeFor();
514
- const guid = makeUuid();
515
- await store.create(ctx(scope.ownerId), record(scope, { guid, status: 'published' }));
516
- await store.delete(ctx(scope.ownerId), guid);
517
- const all = await store.list(ctx(scope.ownerId), { limit: 500 });
518
- expect(all.items.map((r) => r.guid)).not.toContain(guid);
519
- const byProject = await store.listByProject(ctx(scope.ownerId), scope.projectId, {
520
- limit: 500
521
- });
522
- expect(byProject.items.map((r) => r.guid)).not.toContain(guid);
523
- });
524
-
525
- it('deleteByProject soft-deletes every definition in the project, leaving others', async () => {
526
- const store = await createStore();
527
- const scope = await scopeFor();
528
- // Two published definitions in the target project, one in another —
529
- // the cascade must tombstone the target project's and leave the rest.
530
- const a = makeUuid();
531
- const b = makeUuid();
532
- const other = makeUuid();
533
- await store.create(
534
- ctx(scope.ownerId),
535
- record(scope, { guid: a, projectId: scope.projectId, status: 'published' })
536
- );
537
- await store.create(
538
- ctx(scope.ownerId),
539
- record(scope, { guid: b, projectId: scope.projectId, status: 'published' })
540
- );
541
- await store.create(
542
- ctx(scope.ownerId),
543
- record(scope, { guid: other, projectId: scope.secondaryProjectId, status: 'published' })
544
- );
545
-
546
- await store.deleteByProject(ctx(scope.ownerId), scope.projectId);
547
-
548
- // Both target-project definitions gone from every read path…
549
- expect(await store.get(ctx(scope.ownerId), a)).toBeNull();
550
- expect(await store.get(ctx(scope.ownerId), b)).toBeNull();
551
- const targetList = await store.listByProject(ctx(scope.ownerId), scope.projectId, {
552
- limit: 500
553
- });
554
- expect(targetList.items).toHaveLength(0);
555
- const all = await store.list(ctx(scope.ownerId), { limit: 500 });
556
- expect(all.items.map((r) => r.guid)).not.toContain(a);
557
- expect(all.items.map((r) => r.guid)).not.toContain(b);
558
- // …but the other project's definition survives.
559
- expect(await store.get(ctx(scope.ownerId), other)).not.toBeNull();
560
- });
561
-
562
- it('deleteByProject is a no-op when the project has no definitions', async () => {
563
- const store = await createStore();
564
- const scope = await scopeFor();
565
- await expect(
566
- store.deleteByProject(ctx(scope.ownerId), scope.secondaryProjectId)
567
- ).resolves.toBeUndefined();
568
- });
569
-
570
- it('listPublic returns published records, not pending', async () => {
571
- const store = await createStore();
572
- const scope = await scopeFor();
573
- const pub = makeUuid();
574
- const pend = makeUuid();
575
- const dft = makeUuid();
576
- await store.create(ctx(scope.ownerId), record(scope, { guid: pub, status: 'published' }));
577
- await store.create(ctx(scope.ownerId), record(scope, { guid: pend, status: 'pending' }));
578
- await store.create(ctx(scope.ownerId), record(scope, { guid: dft, status: 'draft' }));
579
-
580
- const page = await store.listPublic(ctx(scope.ownerId));
581
- expect(page.items.map((r) => r.guid)).toContain(pub);
582
- expect(page.items.map((r) => r.guid)).not.toContain(pend);
583
- });
584
-
585
- it('pagination respects limit and nextCursor', async () => {
586
- const store = await createStore();
587
- const scope = await scopeFor();
588
- for (let i = 0; i < 5; i++) {
589
- await store.create(ctx(scope.ownerId), record(scope, { displayName: `N${i}` }));
590
- }
591
-
592
- const first = await store.list(ctx(scope.ownerId), { limit: 2 });
593
- expect(first.items.length).toBe(2);
594
- expect(first.nextCursor).toBeTruthy();
595
-
596
- const second = await store.list(ctx(scope.ownerId), { limit: 2, cursor: first.nextCursor });
597
- expect(second.items.length).toBe(2);
598
- const firstGuids = first.items.map((r) => r.guid);
599
- for (const item of second.items) {
600
- expect(firstGuids).not.toContain(item.guid);
601
- }
602
- });
603
-
604
- if (ctxIsolation) {
605
- it('ctx isolation: records created by one user are not visible to another', async () => {
606
- const store = await createStore();
607
- const scope = await scopeFor();
608
- const otherUser = scope.secondaryUserId;
609
- if (!otherUser) throw new Error('ctxIsolation requires scope.secondaryUserId');
610
- const guid = makeUuid();
611
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
612
-
613
- const page = await store.list(ctx(otherUser));
614
- expect(page.items.map((r) => r.guid)).not.toContain(guid);
615
- });
616
-
617
- it('ctx isolation: user cannot get a record they do not own', async () => {
618
- const store = await createStore();
619
- const scope = await scopeFor();
620
- const otherUser = scope.secondaryUserId;
621
- if (!otherUser) throw new Error('ctxIsolation requires scope.secondaryUserId');
622
- const guid = makeUuid();
623
- await store.create(ctx(scope.ownerId), record(scope, { guid }));
624
-
625
- const got = await store.get(ctx(otherUser), guid);
626
- expect(got).toBeNull();
627
- });
628
- }
629
- });
630
- }