was-teaching-server 0.9.1 → 0.11.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 (76) hide show
  1. package/CHANGELOG.md +118 -1
  2. package/README.md +23 -11
  3. package/dist/backends/filesystem.d.ts +247 -4
  4. package/dist/backends/filesystem.d.ts.map +1 -1
  5. package/dist/backends/filesystem.js +711 -81
  6. package/dist/backends/filesystem.js.map +1 -1
  7. package/dist/backends/postgres.d.ts +240 -2
  8. package/dist/backends/postgres.d.ts.map +1 -1
  9. package/dist/backends/postgres.js +800 -42
  10. package/dist/backends/postgres.js.map +1 -1
  11. package/dist/backends/postgresSchema.d.ts.map +1 -1
  12. package/dist/backends/postgresSchema.js +69 -57
  13. package/dist/backends/postgresSchema.js.map +1 -1
  14. package/dist/errors.d.ts +26 -9
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +40 -12
  17. package/dist/errors.js.map +1 -1
  18. package/dist/lib/equalityIndex.d.ts +334 -0
  19. package/dist/lib/equalityIndex.d.ts.map +1 -0
  20. package/dist/lib/equalityIndex.js +552 -0
  21. package/dist/lib/equalityIndex.js.map +1 -0
  22. package/dist/lib/exportManifest.d.ts +9 -0
  23. package/dist/lib/exportManifest.d.ts.map +1 -1
  24. package/dist/lib/exportManifest.js +9 -0
  25. package/dist/lib/exportManifest.js.map +1 -1
  26. package/dist/lib/importTar.d.ts +18 -2
  27. package/dist/lib/importTar.d.ts.map +1 -1
  28. package/dist/lib/importTar.js +77 -4
  29. package/dist/lib/importTar.js.map +1 -1
  30. package/dist/lib/paths.d.ts +33 -0
  31. package/dist/lib/paths.d.ts.map +1 -1
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/paths.js.map +1 -1
  34. package/dist/lib/resourceFileName.d.ts +43 -0
  35. package/dist/lib/resourceFileName.d.ts.map +1 -1
  36. package/dist/lib/resourceFileName.js +63 -0
  37. package/dist/lib/resourceFileName.js.map +1 -1
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/plugin.js +5 -2
  40. package/dist/plugin.js.map +1 -1
  41. package/dist/requests/ChunkRequest.d.ts +104 -0
  42. package/dist/requests/ChunkRequest.d.ts.map +1 -0
  43. package/dist/requests/ChunkRequest.js +396 -0
  44. package/dist/requests/ChunkRequest.js.map +1 -0
  45. package/dist/requests/CollectionRequest.d.ts +22 -5
  46. package/dist/requests/CollectionRequest.d.ts.map +1 -1
  47. package/dist/requests/CollectionRequest.js +192 -11
  48. package/dist/requests/CollectionRequest.js.map +1 -1
  49. package/dist/requests/ResourceRequest.d.ts.map +1 -1
  50. package/dist/requests/ResourceRequest.js +20 -0
  51. package/dist/requests/ResourceRequest.js.map +1 -1
  52. package/dist/requests/SpaceRequest.d.ts +1 -0
  53. package/dist/requests/SpaceRequest.d.ts.map +1 -1
  54. package/dist/requests/SpaceRequest.js +21 -1
  55. package/dist/requests/SpaceRequest.js.map +1 -1
  56. package/dist/routes.d.ts.map +1 -1
  57. package/dist/routes.js +21 -0
  58. package/dist/routes.js.map +1 -1
  59. package/dist/types.d.ts +165 -2
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +19 -18
  62. package/src/backends/filesystem.ts +924 -75
  63. package/src/backends/postgres.ts +1114 -32
  64. package/src/backends/postgresSchema.ts +69 -57
  65. package/src/errors.ts +44 -12
  66. package/src/lib/equalityIndex.ts +769 -0
  67. package/src/lib/importTar.ts +107 -4
  68. package/src/lib/paths.ts +48 -0
  69. package/src/lib/resourceFileName.ts +69 -0
  70. package/src/plugin.ts +5 -2
  71. package/src/requests/ChunkRequest.ts +478 -0
  72. package/src/requests/CollectionRequest.ts +221 -12
  73. package/src/requests/ResourceRequest.ts +20 -0
  74. package/src/requests/SpaceRequest.ts +25 -1
  75. package/src/routes.ts +46 -0
  76. package/src/types.ts +175 -1
@@ -0,0 +1,552 @@
1
+ import { decodeCursor, encodeCursor } from './cursor.js';
2
+ import { clampPageSize, DEFAULT_PAGE_SIZE } from './pagination.js';
3
+ import { InvalidRequestBodyError, UniqueAttributeConflictError } from '../errors.js';
4
+ /**
5
+ * True for a plain (non-null, non-array) object -- the only shape a `content`
6
+ * or `custom` source can be extracted from.
7
+ * @param value {unknown}
8
+ * @returns {boolean}
9
+ */
10
+ function isPlainObject(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ /**
14
+ * True when a JSON value is directly indexable -- a string, number, or boolean
15
+ * (an array of these is handled element-by-element by the caller).
16
+ * @param value {unknown}
17
+ * @returns {boolean}
18
+ */
19
+ function isIndexableValue(value) {
20
+ return (typeof value === 'string' ||
21
+ typeof value === 'number' ||
22
+ typeof value === 'boolean');
23
+ }
24
+ /**
25
+ * Normalizes a Collection Description's declared `indexes` into the uniform
26
+ * {@link NormalizedIndexDeclaration} shape: a bare string entry becomes
27
+ * `{ name, source: 'content', unique: false }`, and an object entry has its
28
+ * optional `source` / `unique` defaulted. An absent declaration normalizes to
29
+ * an empty array. Assumes the declaration has already passed
30
+ * {@link assertSupportedIndexes} (or was read back from storage), so it does no
31
+ * shape validation of its own.
32
+ *
33
+ * @param options {object}
34
+ * @param [options.indexes] {Array<string | CollectionIndexDeclaration>} the
35
+ * Collection Description's declared `indexes`
36
+ * @returns {NormalizedIndexDeclaration[]}
37
+ */
38
+ export function normalizeIndexes({ indexes }) {
39
+ if (indexes === undefined) {
40
+ return [];
41
+ }
42
+ return indexes.map(entry => {
43
+ if (typeof entry === 'string') {
44
+ return { name: entry, source: 'content', unique: false };
45
+ }
46
+ return {
47
+ name: entry.name,
48
+ source: entry.source ?? 'content',
49
+ unique: entry.unique === true
50
+ };
51
+ });
52
+ }
53
+ /**
54
+ * Validates a client-supplied Collection `indexes` declaration (shape only) and
55
+ * returns the value to persist verbatim, or `undefined` when absent (an update
56
+ * then leaves the stored declaration untouched, like `name` / `backend` /
57
+ * `encryption`). Mirrors the validation style of `assertSupportedEncryption` /
58
+ * `assertValidEncryptionEpochs` in `lib/encryption.ts`, rejecting a malformed
59
+ * declaration with `invalid-request-body` (400) and a precise `pointer`. Rules:
60
+ * - `indexes` MUST be an array (an EMPTY array is allowed -- it clears the
61
+ * declaration).
62
+ * - each entry is either a non-empty string (the `content`-sourced shorthand)
63
+ * or an object with a non-empty string `name`, an optional `source` of
64
+ * exactly `'content'` or `'custom'`, and an optional boolean `unique`.
65
+ * - declared names MUST be unique across the array regardless of source
66
+ * (queries refer to attributes by name alone).
67
+ *
68
+ * @param options {object}
69
+ * @param [options.indexes] {unknown} the request body's `indexes` value
70
+ * @param [options.requestName] {string} request name for the 400 error title
71
+ * @returns {Array<string | CollectionIndexDeclaration> | undefined} the
72
+ * declaration to store, or undefined when absent
73
+ */
74
+ export function assertSupportedIndexes({ indexes, requestName }) {
75
+ if (indexes === undefined) {
76
+ return undefined;
77
+ }
78
+ if (!Array.isArray(indexes)) {
79
+ throw new InvalidRequestBodyError({
80
+ requestName,
81
+ detail: 'Collection "indexes" must be an array.',
82
+ pointer: '#/indexes'
83
+ });
84
+ }
85
+ const names = new Set();
86
+ indexes.forEach((entry, entryIndex) => {
87
+ const pointer = `#/indexes/${entryIndex}`;
88
+ let name;
89
+ if (typeof entry === 'string') {
90
+ if (entry.length === 0) {
91
+ throw new InvalidRequestBodyError({
92
+ requestName,
93
+ detail: 'Each "indexes" string entry must be a non-empty attribute name.',
94
+ pointer
95
+ });
96
+ }
97
+ name = entry;
98
+ }
99
+ else if (isPlainObject(entry)) {
100
+ const { name: entryName, source, unique } = entry;
101
+ if (typeof entryName !== 'string' || entryName.length === 0) {
102
+ throw new InvalidRequestBodyError({
103
+ requestName,
104
+ detail: 'Each "indexes" entry must have a non-empty string "name".',
105
+ pointer: `${pointer}/name`
106
+ });
107
+ }
108
+ if (source !== undefined && source !== 'content' && source !== 'custom') {
109
+ throw new InvalidRequestBodyError({
110
+ requestName,
111
+ detail: 'An "indexes" entry "source" must be exactly "content" or "custom".',
112
+ pointer: `${pointer}/source`
113
+ });
114
+ }
115
+ if (unique !== undefined && typeof unique !== 'boolean') {
116
+ throw new InvalidRequestBodyError({
117
+ requestName,
118
+ detail: 'An "indexes" entry "unique" must be a boolean.',
119
+ pointer: `${pointer}/unique`
120
+ });
121
+ }
122
+ name = entryName;
123
+ }
124
+ else {
125
+ throw new InvalidRequestBodyError({
126
+ requestName,
127
+ detail: 'Each "indexes" entry must be a non-empty string or an object with a "name".',
128
+ pointer
129
+ });
130
+ }
131
+ if (names.has(name)) {
132
+ throw new InvalidRequestBodyError({
133
+ requestName,
134
+ detail: `Duplicate "indexes" attribute name "${name}".`,
135
+ pointer: `${pointer}/name`
136
+ });
137
+ }
138
+ names.add(name);
139
+ });
140
+ // Persist verbatim (bare strings and objects alike survive the round-trip);
141
+ // normalization for extraction/matching happens at query/write time.
142
+ return indexes;
143
+ }
144
+ /**
145
+ * Validates and normalizes the `equality` profile's query body fields
146
+ * (everything besides `profile`), throwing `invalid-request-body` (400) on a
147
+ * malformed query. Exactly one of `equals` / `has` is required. Every attribute
148
+ * name appearing in `equals` or `has` MUST be among the declared index names
149
+ * (`indexes`, already normalized) -- a query naming an undeclared attribute is
150
+ * rejected fail-closed (a typo or missing declaration surfaces loudly rather
151
+ * than as a silently-empty page). `limit` is coerced leniently as in
152
+ * `parseBlindedIndexQueryBody` (a non-numeric or `< 1` value falls back to the
153
+ * default; the backend clamps an oversized one), and the opaque `cursor` is
154
+ * validated by the backend's cursor decode (`invalid-cursor` 400).
155
+ *
156
+ * @param options {object}
157
+ * @param options.body {object} the parsed query POST body
158
+ * @param options.indexes {NormalizedIndexDeclaration[]} the Collection's
159
+ * normalized declared indexes (the allowed attribute names)
160
+ * @param [options.requestName] {string}
161
+ * @returns {{ query: EqualityQuery, count: boolean, limit?: number, cursor?: string }}
162
+ */
163
+ export function parseEqualityQueryBody({ body, indexes, requestName }) {
164
+ const { equals, has, count, limit, cursor } = body;
165
+ const declared = new Set(indexes.map(entry => entry.name));
166
+ // Exactly one of `equals` / `has`: supplying neither, or both, is a 400.
167
+ if ((equals === undefined) === (has === undefined)) {
168
+ throw new InvalidRequestBodyError({
169
+ requestName,
170
+ detail: 'An equality query requires exactly one of "equals" or "has".'
171
+ });
172
+ }
173
+ const query = {};
174
+ if (equals !== undefined) {
175
+ if (!Array.isArray(equals) || equals.length === 0) {
176
+ throw new InvalidRequestBodyError({
177
+ requestName,
178
+ detail: '"equals" must be a non-empty array of objects.',
179
+ pointer: '#/equals'
180
+ });
181
+ }
182
+ equals.forEach((element, elementIndex) => {
183
+ const pointer = `#/equals/${elementIndex}`;
184
+ if (!isPlainObject(element)) {
185
+ throw new InvalidRequestBodyError({
186
+ requestName,
187
+ detail: '"equals" elements must be objects mapping declared attribute names to string, number, or boolean values.',
188
+ pointer
189
+ });
190
+ }
191
+ for (const [name, value] of Object.entries(element)) {
192
+ if (!isIndexableValue(value)) {
193
+ throw new InvalidRequestBodyError({
194
+ requestName,
195
+ detail: 'An "equals" attribute value must be a string, number, or boolean.',
196
+ pointer: `${pointer}/${name}`
197
+ });
198
+ }
199
+ if (!declared.has(name)) {
200
+ throw new InvalidRequestBodyError({
201
+ requestName,
202
+ detail: `Attribute "${name}" is not declared in the Collection's indexes.`,
203
+ pointer: `${pointer}/${name}`
204
+ });
205
+ }
206
+ }
207
+ });
208
+ query.equals = equals;
209
+ }
210
+ if (has !== undefined) {
211
+ if (!Array.isArray(has) ||
212
+ has.length === 0 ||
213
+ has.some(name => typeof name !== 'string')) {
214
+ throw new InvalidRequestBodyError({
215
+ requestName,
216
+ detail: '"has" must be a non-empty array of declared attribute names.',
217
+ pointer: '#/has'
218
+ });
219
+ }
220
+ for (const name of has) {
221
+ if (!declared.has(name)) {
222
+ throw new InvalidRequestBodyError({
223
+ requestName,
224
+ detail: `Attribute "${name}" is not declared in the Collection's indexes.`,
225
+ pointer: '#/has'
226
+ });
227
+ }
228
+ }
229
+ query.has = has;
230
+ }
231
+ if (count !== undefined && typeof count !== 'boolean') {
232
+ throw new InvalidRequestBodyError({
233
+ requestName,
234
+ detail: '"count" must be a boolean.',
235
+ pointer: '#/count'
236
+ });
237
+ }
238
+ if (cursor !== undefined && typeof cursor !== 'string') {
239
+ throw new InvalidRequestBodyError({
240
+ requestName,
241
+ detail: '"cursor" must be a string.',
242
+ pointer: '#/cursor'
243
+ });
244
+ }
245
+ // Lenient `limit` coercion, same as the `changes` / `blinded-index` profiles:
246
+ // a non-numeric or `< 1` value is ignored so the backend applies its own
247
+ // default/clamp.
248
+ const parsedLimit = Number(limit);
249
+ const resolvedLimit = Number.isFinite(parsedLimit) && parsedLimit >= 1 ? parsedLimit : undefined;
250
+ return {
251
+ query,
252
+ count: count === true,
253
+ ...(resolvedLimit !== undefined && { limit: resolvedLimit }),
254
+ ...(cursor !== undefined && { cursor })
255
+ };
256
+ }
257
+ /**
258
+ * Extracts a Resource's indexable attribute values per its Collection's
259
+ * declared indexes, keyed by declared name. For each declared entry the
260
+ * top-level member is read from its `source` -- `content` from the stored JSON
261
+ * content, `custom` from the custom metadata object -- each only when that
262
+ * source is a plain object. A member is indexable when it is a string, number,
263
+ * or boolean, or an array of those (an array indexes the Resource under EACH
264
+ * element; a `null`, object, or nested-array element contributes nothing);
265
+ * anything else, an absent member, or a non-object source contributes no entry
266
+ * for that attribute. Strict typing, no coercion: `1`, `"1"`, and `true` stay
267
+ * distinct.
268
+ *
269
+ * @param options {object}
270
+ * @param options.indexes {NormalizedIndexDeclaration[]} the normalized indexes
271
+ * @param [options.content] {unknown} the stored JSON content (a JSON Resource)
272
+ * @param [options.custom] {unknown} the custom metadata object
273
+ * @returns {Map<string, EqualityValue[]>} declared name to its indexable values
274
+ */
275
+ export function extractEqualityAttributes({ indexes, content, custom }) {
276
+ const extracted = new Map();
277
+ for (const declaration of indexes) {
278
+ const source = declaration.source === 'custom' ? custom : content;
279
+ if (!isPlainObject(source)) {
280
+ continue;
281
+ }
282
+ const member = source[declaration.name];
283
+ const values = [];
284
+ if (Array.isArray(member)) {
285
+ for (const element of member) {
286
+ if (isIndexableValue(element)) {
287
+ values.push(element);
288
+ }
289
+ }
290
+ }
291
+ else if (isIndexableValue(member)) {
292
+ values.push(member);
293
+ }
294
+ if (values.length > 0) {
295
+ // A name may be declared once (unique across the array), so no merge is
296
+ // needed; the last declaration for a name wins if that ever changes.
297
+ extracted.set(declaration.name, values);
298
+ }
299
+ }
300
+ return extracted;
301
+ }
302
+ /**
303
+ * True when a candidate Resource matches an equality query. `equals` matches
304
+ * when SOME element has ALL its `{name: value}` pairs present among the
305
+ * candidate's extracted attributes (an empty element matches nothing); `has`
306
+ * matches when EVERY named attribute is present with an indexable value. Strict
307
+ * value equality (`===`), so `1`, `"1"`, and `true` never cross-match; a
308
+ * multi-valued attribute matches when any of its values equals the queried one.
309
+ *
310
+ * @param options {object}
311
+ * @param options.candidate {EqualityCandidate}
312
+ * @param options.query {EqualityQuery}
313
+ * @param options.indexes {NormalizedIndexDeclaration[]}
314
+ * @returns {boolean}
315
+ */
316
+ export function matchesEqualityQuery({ candidate, query, indexes }) {
317
+ const attributes = extractEqualityAttributes({
318
+ indexes,
319
+ content: candidate.content,
320
+ custom: candidate.custom
321
+ });
322
+ if (query.equals !== undefined) {
323
+ return query.equals.some(element => {
324
+ const pairs = Object.entries(element);
325
+ return (pairs.length > 0 &&
326
+ pairs.every(([name, value]) => (attributes.get(name) ?? []).some(held => held === value)));
327
+ });
328
+ }
329
+ return (query.has ?? []).every(name => attributes.has(name));
330
+ }
331
+ /**
332
+ * Evaluates an equality query over a Collection's candidate Resources and
333
+ * shapes the result: a bare `{count}` for a count query, else a page of
334
+ * matching documents in ascending `resourceId` (code-unit) order with the
335
+ * standard opaque-cursor pagination (`limit` clamped to `[1, MAX_PAGE_SIZE]`,
336
+ * default `DEFAULT_PAGE_SIZE`; `cursor` resumes strictly after its anchor id,
337
+ * so paging stays correct if the anchor was deleted between pages). A malformed
338
+ * `cursor` rejects with `invalid-cursor` (400).
339
+ *
340
+ * Backends call this with every live Resource of the Collection (JSON Resources
341
+ * carrying `content`, blobs carrying only `custom`) -- an O(n) full scan,
342
+ * deliberate for these teaching backends (a materialized backend would answer
343
+ * from a JSONB expression index or attribute side-table).
344
+ *
345
+ * @param options {object}
346
+ * @param options.candidates {EqualityCandidate[]}
347
+ * @param options.query {EqualityQuery}
348
+ * @param options.indexes {NormalizedIndexDeclaration[]}
349
+ * @param [options.count] {boolean} return only the match count
350
+ * @param [options.limit] {number} requested page size
351
+ * @param [options.cursor] {string} opaque cursor from a prior page
352
+ * @returns {{ count: number } | EqualityQueryPage}
353
+ */
354
+ export function runEqualityQuery({ candidates, query, indexes, count, limit, cursor }) {
355
+ const matches = candidates
356
+ .filter(candidate => matchesEqualityQuery({ candidate, query, indexes }))
357
+ // Ascending `resourceId` in code-unit order -- the SAME ordering the cursor
358
+ // seek (`resourceId > after`) uses, so the keyset is stable.
359
+ .sort((left, right) => left.resourceId < right.resourceId
360
+ ? -1
361
+ : left.resourceId > right.resourceId
362
+ ? 1
363
+ : 0);
364
+ if (count === true) {
365
+ return { count: matches.length };
366
+ }
367
+ // Seek strictly past the cursor's anchor id.
368
+ let startIndex = 0;
369
+ if (cursor !== undefined) {
370
+ const { after } = decodeCursor(cursor);
371
+ const found = matches.findIndex(({ resourceId }) => resourceId > after);
372
+ startIndex = found === -1 ? matches.length : found;
373
+ }
374
+ const pageSize = limit === undefined ? DEFAULT_PAGE_SIZE : clampPageSize(limit);
375
+ // Take `pageSize + 1` to detect a further page without a second pass.
376
+ const window = matches.slice(startIndex, startIndex + pageSize + 1);
377
+ const hasMore = window.length > pageSize;
378
+ const page = hasMore ? window.slice(0, pageSize) : window;
379
+ return {
380
+ documents: page.map(candidate => ({
381
+ id: candidate.resourceId,
382
+ // `data` present iff the Resource has JSON content (absent for a blob);
383
+ // `custom` present when the Resource carries a custom metadata object.
384
+ ...(candidate.content !== undefined && { data: candidate.content }),
385
+ ...(isPlainObject(candidate.custom) && { custom: candidate.custom })
386
+ })),
387
+ hasMore,
388
+ ...(hasMore && {
389
+ cursor: encodeCursor(page[page.length - 1].resourceId)
390
+ })
391
+ };
392
+ }
393
+ /**
394
+ * Collects a Resource's `unique: true` attribute claims as `(name, value)`
395
+ * pairs -- the claims the per-Collection uniqueness invariant protects. Only
396
+ * declared entries carrying `unique: true` are considered; each indexable value
397
+ * (an array value claims EACH element) becomes one claim. A Resource whose
398
+ * unique attribute is absent or non-indexable makes no claim.
399
+ *
400
+ * @param options {object}
401
+ * @param options.indexes {NormalizedIndexDeclaration[]}
402
+ * @param [options.content] {unknown} the stored JSON content
403
+ * @param [options.custom] {unknown} the custom metadata object
404
+ * @returns {Array<{ name: string, value: EqualityValue }>}
405
+ */
406
+ export function collectUniqueEqualityTerms({ indexes, content, custom }) {
407
+ const uniqueDeclarations = indexes.filter(declaration => declaration.unique);
408
+ if (uniqueDeclarations.length === 0) {
409
+ return [];
410
+ }
411
+ const extracted = extractEqualityAttributes({
412
+ indexes: uniqueDeclarations,
413
+ content,
414
+ custom
415
+ });
416
+ const terms = [];
417
+ for (const [name, values] of extracted) {
418
+ for (const value of values) {
419
+ terms.push({ name, value });
420
+ }
421
+ }
422
+ return terms;
423
+ }
424
+ /**
425
+ * Enforces the per-Collection unique-attribute invariant on a write: a
426
+ * `unique: true` declared attribute's `(name, value)` claim may be held by at
427
+ * most one live Resource in the Collection. Because the declaration is
428
+ * Collection-level, a conflict does NOT require the other side to opt in -- ANY
429
+ * other live Resource whose extracted value for that attribute equals the claim
430
+ * conflicts. `candidates` MUST already exclude the Resource being written, so a
431
+ * Resource re-asserting its own existing value never self-conflicts. Throws
432
+ * `UniqueAttributeConflictError` (409) on a conflict; a Resource with no unique
433
+ * claim never throws.
434
+ *
435
+ * The caller is responsible for making the check-and-write atomic (the
436
+ * filesystem backend serializes claim-carrying writes per Collection; the
437
+ * Postgres backend takes a per-Collection advisory lock in the write
438
+ * transaction), the same pattern as `assertNoUniqueBlindedConflict`.
439
+ *
440
+ * @param options {object}
441
+ * @param options.indexes {NormalizedIndexDeclaration[]}
442
+ * @param [options.content] {unknown} the incoming Resource's content
443
+ * @param [options.custom] {unknown} the incoming Resource's custom metadata
444
+ * @param options.candidates {EqualityCandidate[]} the Collection's OTHER live
445
+ * Resources (the one being written already excluded)
446
+ * @returns {void}
447
+ */
448
+ export function assertNoUniqueEqualityConflict({ indexes, content, custom, candidates }) {
449
+ const incoming = collectUniqueEqualityTerms({ indexes, content, custom });
450
+ if (incoming.length === 0) {
451
+ return;
452
+ }
453
+ // Key the claims unambiguously (a name or a string value may itself contain
454
+ // any delimiter, and the value's JSON type must stay distinct -- `1` vs `"1"`
455
+ // -- so a joined string could collide).
456
+ const claimed = new Set(incoming.map(term => JSON.stringify([term.name, term.value])));
457
+ for (const candidate of candidates) {
458
+ const held = collectUniqueEqualityTerms({
459
+ indexes,
460
+ content: candidate.content,
461
+ custom: candidate.custom
462
+ });
463
+ for (const term of held) {
464
+ if (claimed.has(JSON.stringify([term.name, term.value]))) {
465
+ throw new UniqueAttributeConflictError({ variant: 'equality' });
466
+ }
467
+ }
468
+ }
469
+ }
470
+ /**
471
+ * Scans a Collection's candidates for an existing violation of a unique claim:
472
+ * two DIFFERENT Resources holding the same `(name, value)` under the
473
+ * `unique`-declared entries. Returns the first such `{ name, value }`, or
474
+ * `undefined` when none exists. Used when a Collection update ADDS a unique
475
+ * claim over already-stored Resources (the declare-time conflict scan); the
476
+ * request layer rejects a found violation with `id-conflict` (409).
477
+ *
478
+ * @param options {object}
479
+ * @param options.indexes {NormalizedIndexDeclaration[]}
480
+ * @param options.candidates {EqualityCandidate[]} the Collection's live
481
+ * Resources
482
+ * @returns {{ name: string, value: EqualityValue } | undefined}
483
+ */
484
+ export function findEqualityUniqueViolation({ indexes, candidates }) {
485
+ const uniqueDeclarations = indexes.filter(declaration => declaration.unique);
486
+ if (uniqueDeclarations.length === 0) {
487
+ return undefined;
488
+ }
489
+ // Map each seen claim key to the resourceId holding it; a second, different
490
+ // holder is the violation.
491
+ const holders = new Map();
492
+ for (const candidate of candidates) {
493
+ const held = collectUniqueEqualityTerms({
494
+ indexes: uniqueDeclarations,
495
+ content: candidate.content,
496
+ custom: candidate.custom
497
+ });
498
+ for (const term of held) {
499
+ const key = JSON.stringify([term.name, term.value]);
500
+ const existing = holders.get(key);
501
+ if (existing !== undefined && existing !== candidate.resourceId) {
502
+ return { name: term.name, value: term.value };
503
+ }
504
+ holders.set(key, candidate.resourceId);
505
+ }
506
+ }
507
+ return undefined;
508
+ }
509
+ /**
510
+ * Collects the GET List Collection `filter[<name>]=<value>` equality filters
511
+ * from a Fastify parsed querystring object -- the anonymous-cacheable entry
512
+ * point over the same equality machinery as the POST profile. Fastify's default
513
+ * querystring parser leaves bracket keys literal and percent-decodes them, so a
514
+ * `filter[parentId]=first-post` query arrives as the key `filter[parentId]`.
515
+ * Returns a `{ <name>: <value> }` map, or `undefined` when no `filter[...]` key
516
+ * is present (the ordinary listing path). A repeated same attribute (the parser
517
+ * yields an array value) is rejected with `invalid-request-body` (400) -- v1
518
+ * admits only a single value per attribute. Attribute names are NOT validated
519
+ * against the Collection's declarations here; the handler does that once it has
520
+ * fetched the description.
521
+ *
522
+ * @param options {object}
523
+ * @param options.query {Record<string, string | string[] | undefined>} the
524
+ * Fastify-parsed querystring object
525
+ * @param [options.requestName] {string}
526
+ * @returns {Record<string, string> | undefined}
527
+ */
528
+ export function parseListFilter({ query, requestName }) {
529
+ const filters = {};
530
+ let found = false;
531
+ for (const [key, value] of Object.entries(query)) {
532
+ const match = /^filter\[(.+)\]$/.exec(key);
533
+ if (!match) {
534
+ continue;
535
+ }
536
+ const name = match[1];
537
+ found = true;
538
+ if (Array.isArray(value)) {
539
+ throw new InvalidRequestBodyError({
540
+ requestName,
541
+ detail: `Filter attribute "${name}" may not be repeated.`,
542
+ pointer: `#/filter/${name}`
543
+ });
544
+ }
545
+ if (value === undefined) {
546
+ continue;
547
+ }
548
+ filters[name] = value;
549
+ }
550
+ return found ? filters : undefined;
551
+ }
552
+ //# sourceMappingURL=equalityIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"equalityIndex.js","sourceRoot":"","sources":["../../src/lib/equalityIndex.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Fixed `mtime` for every entry in an export archive. `tar-stream` defaults a
3
+ * header's `mtime` to the wall-clock time it was packed, which makes two
4
+ * exports of an unchanged Space differ whenever they straddle a one-second
5
+ * boundary. Pinning it to the Unix epoch makes an export a pure function of
6
+ * the Space's contents (byte-reproducible and diff-stable); the timestamp
7
+ * carries no meaning a consumer relies on.
8
+ */
9
+ export declare const EXPORT_ENTRY_MTIME: Date;
1
10
  /**
2
11
  * One top-level entry of the Space being exported, in archive order: a
3
12
  * Space-level file (`files` absent) or a Collection directory with its ordered
@@ -1 +1 @@
1
- {"version":3,"file":"exportManifest.d.ts","sourceRoot":"","sources":["../../src/lib/exportManifest.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,OAAO,EACP,eAAoB,EACrB,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B,GAAG,MAAM,CAiCT"}
1
+ {"version":3,"file":"exportManifest.d.ts","sourceRoot":"","sources":["../../src/lib/exportManifest.ts"],"names":[],"mappings":"AAgBA;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,MAAc,CAAA;AAE7C;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,OAAO,EACP,eAAoB,EACrB,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B,GAAG,MAAM,CAiCT"}
@@ -6,6 +6,15 @@
6
6
  * this module is the single home for the manifest that describes it.
7
7
  */
8
8
  import { UBC_MANIFEST_URL, SPACE_URL, COLLECTION_URL, RESOURCE_URL, POLICY_URL, META_URL } from '../config.default.js';
9
+ /**
10
+ * Fixed `mtime` for every entry in an export archive. `tar-stream` defaults a
11
+ * header's `mtime` to the wall-clock time it was packed, which makes two
12
+ * exports of an unchanged Space differ whenever they straddle a one-second
13
+ * boundary. Pinning it to the Unix epoch makes an export a pure function of
14
+ * the Space's contents (byte-reproducible and diff-stable); the timestamp
15
+ * carries no meaning a consumer relies on.
16
+ */
17
+ export const EXPORT_ENTRY_MTIME = new Date(0);
9
18
  /**
10
19
  * Classifies one Collection-dir file name into its manifest entry: the known
11
20
  * dot-file kinds and resource representations get a documenting `url`, anything
@@ -1 +1 @@
1
- {"version":3,"file":"exportManifest.js","sourceRoot":"","sources":["../../src/lib/exportManifest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,cAAc,EACd,YAAY,EACZ,UAAU,EACV,QAAQ,EACT,MAAM,sBAAsB,CAAA;AAa7B;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,QAAgB;IAC/C,IAAI,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,CAAA;IAChD,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,CAAA;IAC5C,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAA;IAC1C,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,CAAA;IAC9C,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,OAAO,EACP,OAAO,EACP,eAAe,GAAG,EAAE,EAKrB;IACC,MAAM,aAAa,GAAc,EAAE,CAAA;IACnC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC9B,wDAAwD;YACxD,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAC9B,SAAQ;QACV,CAAC;QACD,aAAa,CAAC,IAAI,CAAC;YACjB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;gBACZ,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,uBAAuB,CAAC;aACnD;SACF,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,aAAa,EAAE,KAAK;QACpB,QAAQ,EAAE;YACR,cAAc,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE;YACzC,GAAG,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,IAAI;gBAChC,WAAW,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE;aAChD,CAAC;YACF,KAAK,EAAE;gBACL,GAAG,EAAE,SAAS;gBACd,QAAQ,EAAE;oBACR,CAAC,OAAO,CAAC,EAAE;wBACT,GAAG,EAAE,SAAS;wBACd,QAAQ,EAAE,aAAa;qBACxB;iBACF;aACF;SACF;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"exportManifest.js","sourceRoot":"","sources":["../../src/lib/exportManifest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,cAAc,EACd,YAAY,EACZ,UAAU,EACV,QAAQ,EACT,MAAM,sBAAsB,CAAA;AAE7B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAA;AAa7C;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,QAAgB;IAC/C,IAAI,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,CAAA;IAChD,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,CAAA;IAC5C,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAA;IAC1C,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,CAAA;IAC9C,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,OAAO,EACP,OAAO,EACP,eAAe,GAAG,EAAE,EAKrB;IACC,MAAM,aAAa,GAAc,EAAE,CAAA;IACnC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC9B,wDAAwD;YACxD,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAC9B,SAAQ;QACV,CAAC;QACD,aAAa,CAAC,IAAI,CAAC;YACjB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;gBACZ,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,uBAAuB,CAAC;aACnD;SACF,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,aAAa,EAAE,KAAK;QACpB,QAAQ,EAAE;YACR,cAAc,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE;YACzC,GAAG,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,IAAI;gBAChC,WAAW,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE;aAChD,CAAC;YACF,KAAK,EAAE;gBACL,GAAG,EAAE,SAAS;gBACd,QAAQ,EAAE;oBACR,CAAC,OAAO,CAAC,EAAE;wBACT,GAAG,EAAE,SAAS;wBACd,QAAQ,EAAE,aAAa;qBACxB;iBACF;aACF;SACF;KACF,CAAA;AACH,CAAC"}
@@ -18,6 +18,18 @@ export interface ImportPlanResource {
18
18
  resourceId: string;
19
19
  body: Buffer;
20
20
  }
21
+ /**
22
+ * One chunk file of a chunked Resource staged for import (the `chunked-streams`
23
+ * feature): the raw bytes carried verbatim, keyed by the parent `resourceId`
24
+ * (decoded from the `.chunks.<encId>/` directory name) and the chunk file's
25
+ * basename inside that directory (`r.<index>...` bytes or its `.meta.<index>.json`
26
+ * version sidecar).
27
+ */
28
+ export interface ImportPlanChunkFile {
29
+ resourceId: string;
30
+ fileName: string;
31
+ body: Buffer;
32
+ }
21
33
  /** One collection (plus its resources and policies) staged for import. */
22
34
  export interface ImportPlanCollection {
23
35
  collectionId: string;
@@ -29,6 +41,8 @@ export interface ImportPlanCollection {
29
41
  resourcePolicies: Map<string, PolicyDocument>;
30
42
  /** Resource metadata sidecars (raw `.meta.<id>.json` bytes), keyed by resourceId. */
31
43
  resourceMetadata: Map<string, Buffer>;
44
+ /** Chunk files of chunked Resources in this Collection, carried verbatim. */
45
+ chunkFiles: ImportPlanChunkFile[];
32
46
  }
33
47
  /** The merge plan produced by {@link buildImportPlan}. */
34
48
  export interface ImportPlan {
@@ -69,10 +83,12 @@ export declare function validateManifest(entries: Map<string, TarEntry>): void;
69
83
  * - space/<sourceSpaceId>/<collectionId>/.policy.<collectionId>.json (collection policy)
70
84
  * - space/<sourceSpaceId>/<collectionId>/.policy.<resourceId>.json (resource policy)
71
85
  * - space/<sourceSpaceId>/<collectionId>/r.<resourceId>.<encodedContentType>.<ext>
86
+ * - space/<sourceSpaceId>/<collectionId>/.chunks.<encodedResourceId>/<chunkFile>
87
+ * (a chunked Resource's chunk files; the `chunked-streams` feature)
72
88
  *
73
89
  * The source space id in the path may differ from the import target; collection
74
- * metadata, r.* resource files, and `.policy.*` policy files are merged into the
75
- * destination.
90
+ * metadata, r.* resource files, `.chunks.*` chunk files, and `.policy.*` policy
91
+ * files are merged into the destination.
76
92
  *
77
93
  * @param entries {Map<string, TarEntry>}
78
94
  * @returns {ImportPlan}