@haverstack/core 0.42.0 → 0.44.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/README.md +6 -9
- package/dist/access.d.ts +36 -34
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +51 -100
- package/dist/access.js.map +1 -1
- package/dist/adapter/combine.d.ts.map +1 -1
- package/dist/adapter/combine.js +14 -7
- package/dist/adapter/combine.js.map +1 -1
- package/dist/adapter/copy.d.ts +27 -0
- package/dist/adapter/copy.d.ts.map +1 -0
- package/dist/adapter/copy.js +105 -0
- package/dist/adapter/copy.js.map +1 -0
- package/dist/adapter/index.d.ts +9 -4
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +6 -3
- package/dist/adapter/index.js.map +1 -1
- package/dist/adapter/reach.d.ts +14 -0
- package/dist/adapter/reach.d.ts.map +1 -0
- package/dist/adapter/reach.js +31 -0
- package/dist/adapter/reach.js.map +1 -0
- package/dist/adapter/seed.d.ts +13 -0
- package/dist/adapter/seed.d.ts.map +1 -0
- package/dist/adapter/seed.js +26 -0
- package/dist/adapter/seed.js.map +1 -0
- package/dist/associations/identity.d.ts +18 -42
- package/dist/associations/identity.d.ts.map +1 -1
- package/dist/associations/identity.js +60 -84
- package/dist/associations/identity.js.map +1 -1
- package/dist/associations/validation.d.ts +1 -8
- package/dist/associations/validation.d.ts.map +1 -1
- package/dist/associations/validation.js +3 -30
- package/dist/associations/validation.js.map +1 -1
- package/dist/collection.d.ts +7 -11
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +24 -34
- package/dist/collection.js.map +1 -1
- package/dist/content-match.d.ts +2 -2
- package/dist/content-match.js +2 -2
- package/dist/errors.d.ts +41 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +56 -15
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +18 -6
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +48 -37
- package/dist/grants.js.map +1 -1
- package/dist/hash.d.ts +8 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +14 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +29 -5
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +69 -44
- package/dist/install.js.map +1 -1
- package/dist/lazy.d.ts +13 -0
- package/dist/lazy.d.ts.map +1 -0
- package/dist/lazy.js +27 -0
- package/dist/lazy.js.map +1 -0
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +1 -1
- package/dist/query-validation.d.ts +41 -28
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +85 -71
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +12 -36
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +9 -98
- package/dist/record-changes.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -5
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack/authority-snapshot.d.ts +37 -0
- package/dist/scoped-stack/authority-snapshot.d.ts.map +1 -0
- package/dist/scoped-stack/authority-snapshot.js +49 -0
- package/dist/scoped-stack/authority-snapshot.js.map +1 -0
- package/dist/scoped-stack/authority.d.ts +69 -51
- package/dist/scoped-stack/authority.d.ts.map +1 -1
- package/dist/scoped-stack/authority.js +189 -153
- package/dist/scoped-stack/authority.js.map +1 -1
- package/dist/scoped-stack/family-policy.d.ts +59 -0
- package/dist/scoped-stack/family-policy.d.ts.map +1 -0
- package/dist/scoped-stack/family-policy.js +68 -0
- package/dist/scoped-stack/family-policy.js.map +1 -0
- package/dist/scoped-stack/scoped-stack.d.ts +73 -86
- package/dist/scoped-stack/scoped-stack.d.ts.map +1 -1
- package/dist/scoped-stack/scoped-stack.js +279 -337
- package/dist/scoped-stack/scoped-stack.js.map +1 -1
- package/dist/scoped-stack/subscription.d.ts +14 -33
- package/dist/scoped-stack/subscription.d.ts.map +1 -1
- package/dist/scoped-stack/subscription.js +16 -57
- package/dist/scoped-stack/subscription.js.map +1 -1
- package/dist/stack/arguments.d.ts +27 -0
- package/dist/stack/arguments.d.ts.map +1 -0
- package/dist/stack/arguments.js +53 -0
- package/dist/stack/arguments.js.map +1 -0
- package/dist/stack/attachments.d.ts +1 -1
- package/dist/stack/attachments.js +1 -1
- package/dist/stack/changes.d.ts +4 -10
- package/dist/stack/changes.d.ts.map +1 -1
- package/dist/stack/changes.js +8 -8
- package/dist/stack/changes.js.map +1 -1
- package/dist/stack/client.d.ts +34 -58
- package/dist/stack/client.d.ts.map +1 -1
- package/dist/stack/field-rules.d.ts +27 -0
- package/dist/stack/field-rules.d.ts.map +1 -0
- package/dist/stack/field-rules.js +50 -0
- package/dist/stack/field-rules.js.map +1 -0
- package/dist/stack/install-app.d.ts +2 -2
- package/dist/stack/install-app.d.ts.map +1 -1
- package/dist/stack/install-app.js +16 -14
- package/dist/stack/install-app.js.map +1 -1
- package/dist/stack/integrity.d.ts +9 -53
- package/dist/stack/integrity.d.ts.map +1 -1
- package/dist/stack/integrity.js +47 -107
- package/dist/stack/integrity.js.map +1 -1
- package/dist/stack/limits.d.ts +1 -1
- package/dist/stack/limits.js +1 -1
- package/dist/stack/reads.d.ts +9 -5
- package/dist/stack/reads.d.ts.map +1 -1
- package/dist/stack/reads.js +24 -4
- package/dist/stack/reads.js.map +1 -1
- package/dist/stack/stack.d.ts +84 -79
- package/dist/stack/stack.d.ts.map +1 -1
- package/dist/stack/stack.js +236 -306
- package/dist/stack/stack.js.map +1 -1
- package/dist/stack/system-types.d.ts.map +1 -1
- package/dist/stack/system-types.js +1 -3
- package/dist/stack/system-types.js.map +1 -1
- package/dist/stack/type-grants.d.ts.map +1 -1
- package/dist/stack/type-grants.js +9 -6
- package/dist/stack/type-grants.js.map +1 -1
- package/dist/stack/write-expectation.d.ts +13 -12
- package/dist/stack/write-expectation.d.ts.map +1 -1
- package/dist/stack/write-expectation.js.map +1 -1
- package/dist/stack/write-gate.d.ts +23 -0
- package/dist/stack/write-gate.d.ts.map +1 -0
- package/dist/stack/write-gate.js +17 -0
- package/dist/stack/write-gate.js.map +1 -0
- package/dist/testing.d.ts +37 -24
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +176 -153
- package/dist/testing.js.map +1 -1
- package/dist/types/adapter.d.ts +115 -43
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/associations.d.ts +1 -17
- package/dist/types/associations.d.ts.map +1 -1
- package/dist/types/associations.js +1 -8
- package/dist/types/associations.js.map +1 -1
- package/dist/types/capabilities.d.ts +8 -8
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/events.d.ts +3 -8
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/query.d.ts +4 -4
- package/dist/types/system.d.ts +2 -8
- package/dist/types/system.d.ts.map +1 -1
- package/dist/types/system.js.map +1 -1
- package/dist/validate.d.ts +2 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +2 -1
- package/dist/validate.js.map +1 -1
- package/dist/wire/body.d.ts +32 -9
- package/dist/wire/body.d.ts.map +1 -1
- package/dist/wire/body.js +69 -8
- package/dist/wire/body.js.map +1 -1
- package/dist/wire/index.d.ts +4 -4
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +3 -3
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/record.d.ts +1 -8
- package/dist/wire/record.d.ts.map +1 -1
- package/dist/wire/record.js +0 -48
- package/dist/wire/record.js.map +1 -1
- package/dist/wire/request.d.ts +18 -18
- package/dist/wire/request.d.ts.map +1 -1
- package/dist/wire/request.js +41 -36
- package/dist/wire/request.js.map +1 -1
- package/package.json +1 -1
- package/dist/stack/identity-bindings.d.ts +0 -13
- package/dist/stack/identity-bindings.d.ts.map +0 -1
- package/dist/stack/identity-bindings.js +0 -36
- package/dist/stack/identity-bindings.js.map +0 -1
package/dist/stack/stack.js
CHANGED
|
@@ -21,28 +21,29 @@ import { hashSchema, isCompatible, parseTypeId, baseIdOf, diffSchemas, fileRefFi
|
|
|
21
21
|
import { dropAbsentFields, validateContent, validateContentKeys, validatePatchValues, validateReservedKeys, validateSchemaFieldNames, validateSchemaReservedNames, validateSchemaShape, } from '../validate.js';
|
|
22
22
|
import { applyMergePatch } from '../merge.js';
|
|
23
23
|
import { compareRecordedAttachments } from '../attachment-records.js';
|
|
24
|
-
import {
|
|
24
|
+
import { createAssociations, validatePermissions } from '../access.js';
|
|
25
25
|
import { ChangeEmitter, RelayDelivery, PendingChange, assertSinceUsable } from './changes.js';
|
|
26
26
|
import { SYSTEM_TYPES } from '../types/index.js';
|
|
27
|
-
import { UseAfterCloseError, InvalidAdapterError, StackConflictError, StackMigrationError, StackNotFoundError, StackBadRequestError, StackSchemaDriftError, StackValidationError, StackVersionConflictError, ARGUMENTS_INVALID, SCHEMA_INVALID, } from '../errors.js';
|
|
28
|
-
import { assertQueryCapabilities,
|
|
29
|
-
import { assertAssociationEdits,
|
|
30
|
-
import { applyAssociationEdits, associationDelta
|
|
31
|
-
import { validateGrantee, validateGrantBaseId } from '../grants.js';
|
|
32
|
-
import { bindingFieldsOf } from './identity-bindings.js';
|
|
27
|
+
import { ElementSetMovedError, UseAfterCloseError, ScopeUnavailableError, InvalidAdapterError, StackConflictError, StackMigrationError, StackNotFoundError, StackBadRequestError, StackSchemaDriftError, StackValidationError, StackVersionConflictError, ARGUMENTS_INVALID, SCHEMA_INVALID, softDeletedRefusal, } from '../errors.js';
|
|
28
|
+
import { assertQueryCapabilities, assertValidBaseIdFilter, assertValidJournalQuery, assertValidVersionsQuery, resolveQuery, splitQuery, filtersContent, assertFamilyId, } from '../query-validation.js';
|
|
29
|
+
import { assertAssociationEdits, assertAuthorityAssociations, assertDataAssociations, validateAssociations, } from '../associations/validation.js';
|
|
30
|
+
import { applyAssociationEdits, associationDelta } from '../associations/identity.js';
|
|
31
|
+
import { validateGrantActions, validateGrantee, validateGrantBaseId } from '../grants.js';
|
|
33
32
|
import { validateInstall } from '../install.js';
|
|
34
33
|
import { assertAttachmentSize, assertContentSize } from './limits.js';
|
|
35
34
|
import { validateRecordId, validateClockField, validateIdTimestampSkew, DEFAULT_ID_TIMESTAMP_SKEW_MS, } from './record-id.js';
|
|
36
|
-
import { queryAllPages, lookupEntityByDid
|
|
37
|
-
import {
|
|
35
|
+
import { queryAllPages, lookupEntityByDid } from './reads.js';
|
|
36
|
+
import { contentEqual } from '../record-changes.js';
|
|
37
|
+
import { assertAssociationSet, assertContentPatch, assertParentArgument, assertUnlistedArgument, } from './arguments.js';
|
|
38
38
|
import { ScopedStack, scopeToken } from '../scoped-stack/scoped-stack.js';
|
|
39
39
|
import { MigrationRegistry } from './migrations.js';
|
|
40
40
|
import * as apps from './install-app.js';
|
|
41
41
|
import * as typeGrants from './type-grants.js';
|
|
42
42
|
import * as attachments from './attachments.js';
|
|
43
|
-
import {
|
|
43
|
+
import { assertGroupAdminRemains, assertNoParentCycle, assertParentExists, checkAttachmentAssociationPointers, checkContentWrite, } from './integrity.js';
|
|
44
44
|
import { systemTypeDefinitions } from './system-types.js';
|
|
45
45
|
import { checkFamily, checkSnapshotFamily, checkStoredVersion, WRITE_EXPECTATION, } from './write-expectation.js';
|
|
46
|
+
import { WRITE_GATE } from './write-gate.js';
|
|
46
47
|
import { createCollection } from '../collection.js';
|
|
47
48
|
// -------------------------------------------------------
|
|
48
49
|
// Supporting definitions
|
|
@@ -66,8 +67,8 @@ function normalizeActor(actor) {
|
|
|
66
67
|
? { subjectId }
|
|
67
68
|
: { subjectId, principalId };
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
-
const
|
|
70
|
+
/** How many Records migrateAll() reads and migrates per page. */
|
|
71
|
+
const MIGRATE_ALL_BATCH = 100;
|
|
71
72
|
/**
|
|
72
73
|
* One refusal for every problem a call carries, content and arguments alike,
|
|
73
74
|
* so a caller fixes them in one round trip. Content is itself an argument,
|
|
@@ -90,6 +91,7 @@ function typedContentErrors(typeId, content, schema) {
|
|
|
90
91
|
...validateContent(content, schema),
|
|
91
92
|
...validateGrantee(typeId, content),
|
|
92
93
|
...validateGrantBaseId(typeId, content),
|
|
94
|
+
...validateGrantActions(typeId, content),
|
|
93
95
|
...validateInstall(typeId, content),
|
|
94
96
|
];
|
|
95
97
|
}
|
|
@@ -123,7 +125,7 @@ export class Stack {
|
|
|
123
125
|
/**
|
|
124
126
|
* Announce a mutation that has already been persisted. Called after the
|
|
125
127
|
* adapter write resolves and before the mutating method settles, so
|
|
126
|
-
* `await stack.
|
|
128
|
+
* `await stack.patchContent(...)` guarantees subscribers have been notified —
|
|
127
129
|
* and nothing about work they deferred. A handler cannot fail the write:
|
|
128
130
|
* there is nothing left to fail. See docs/spec/events.md § Handlers.
|
|
129
131
|
*/
|
|
@@ -151,7 +153,7 @@ export class Stack {
|
|
|
151
153
|
return type;
|
|
152
154
|
}
|
|
153
155
|
/**
|
|
154
|
-
* Open a Stack over an adapter. Reads ownerEntityId
|
|
156
|
+
* Open a Stack over an adapter. Reads ownerEntityId from the adapter.
|
|
155
157
|
*/
|
|
156
158
|
static async open(adapter, opts = {}) {
|
|
157
159
|
if (!adapter.ownerEntityId) {
|
|
@@ -192,9 +194,6 @@ export class Stack {
|
|
|
192
194
|
async getOwnerEntity() {
|
|
193
195
|
return this.getEntityByDid(this.ownerEntityId);
|
|
194
196
|
}
|
|
195
|
-
get timezone() {
|
|
196
|
-
return this.adapter.timezone;
|
|
197
|
-
}
|
|
198
197
|
get capabilities() {
|
|
199
198
|
return this.adapter.capabilities;
|
|
200
199
|
}
|
|
@@ -218,8 +217,24 @@ export class Stack {
|
|
|
218
217
|
const { subjectId, principalId = subjectId } = normalizeActor(actor);
|
|
219
218
|
return this.scope(principalId, subjectId);
|
|
220
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* A scoped view needs a stack that owns its storage: one whose adapter
|
|
222
|
+
* queries within a reach and relays no changes from elsewhere. A
|
|
223
|
+
* server-backed stack is scoped by the session that opened it instead.
|
|
224
|
+
* See docs/spec/access-control.md § Enforcement: Stack.asEntity().
|
|
225
|
+
*/
|
|
221
226
|
scope(principalId, subjectId) {
|
|
222
227
|
this.assertOpen();
|
|
228
|
+
const queryReadable = this.adapter.queryReadable?.bind(this.adapter);
|
|
229
|
+
if (!queryReadable) {
|
|
230
|
+
throw new ScopeUnavailableError('This stack cannot be scoped: its adapter does not implement queryReadable(), so a ' +
|
|
231
|
+
"query could not be kept to the requester's reach. Every local adapter must; a " +
|
|
232
|
+
'server-backed stack is scoped by opening it with the session you mean.');
|
|
233
|
+
}
|
|
234
|
+
if (this.relaysChanges) {
|
|
235
|
+
throw new ScopeUnavailableError('This stack cannot be scoped: it relays changes made elsewhere, whose frames were ' +
|
|
236
|
+
'scoped by the session that opened the feed. Open it with the session you mean.');
|
|
237
|
+
}
|
|
223
238
|
return new ScopedStack(scopeToken, {
|
|
224
239
|
stack: this,
|
|
225
240
|
principalId,
|
|
@@ -228,8 +243,8 @@ export class Stack {
|
|
|
228
243
|
adapter: this.adapter,
|
|
229
244
|
changes: this.changes,
|
|
230
245
|
assertOpen: () => this.assertOpen(),
|
|
231
|
-
relaysChanges: this.relaysChanges,
|
|
232
246
|
migrations: this.migrations,
|
|
247
|
+
queryReadable: (split, reach) => this.dispatchQuery(split, (resolved) => queryReadable(resolved, reach)),
|
|
233
248
|
});
|
|
234
249
|
}
|
|
235
250
|
collection(handle) {
|
|
@@ -335,7 +350,7 @@ export class Stack {
|
|
|
335
350
|
async migrateAll(baseId, opts = {}) {
|
|
336
351
|
this.assertOpen();
|
|
337
352
|
assertFamilyId(baseId, 'migrateAll');
|
|
338
|
-
const familyTypeIds = await this.
|
|
353
|
+
const familyTypeIds = typeIdsOfFamilies(await this.adapter.listTypes(), [baseId]);
|
|
339
354
|
if (familyTypeIds.length === 0) {
|
|
340
355
|
throw new StackMigrationError(`migrateAll: no registered types found for baseId "${baseId}"`);
|
|
341
356
|
}
|
|
@@ -355,11 +370,11 @@ export class Stack {
|
|
|
355
370
|
}
|
|
356
371
|
let cursor;
|
|
357
372
|
do {
|
|
358
|
-
const result = await this.adapter.queryRecords({
|
|
373
|
+
const result = await this.adapter.queryRecords(resolveQuery({
|
|
359
374
|
filter: { typeId, includeDeleted: true, includeUnlisted: !listedOnly },
|
|
360
|
-
limit:
|
|
375
|
+
limit: MIGRATE_ALL_BATCH,
|
|
361
376
|
cursor,
|
|
362
|
-
});
|
|
377
|
+
}));
|
|
363
378
|
for (const record of result.records) {
|
|
364
379
|
if (listedOnly && record.deletedAt) {
|
|
365
380
|
skipped++;
|
|
@@ -425,11 +440,8 @@ export class Stack {
|
|
|
425
440
|
];
|
|
426
441
|
throwValidation(contentErrors, argumentErrors);
|
|
427
442
|
assertContentSize(content, this.capabilities.limits.contentBytes, 'Content');
|
|
428
|
-
|
|
429
|
-
await checkAttachmentMimeTypeOnCreate(this, content);
|
|
430
|
-
}
|
|
443
|
+
await checkContentWrite(this, typeId, content);
|
|
431
444
|
await checkAttachmentAssociationPointers(this.readRecord, opts.associations);
|
|
432
|
-
await checkBindingsOnCreate(this, typeId, content);
|
|
433
445
|
if (opts.id !== undefined) {
|
|
434
446
|
validateRecordId(opts.id);
|
|
435
447
|
// Only when both are explicit: an `id` alone is a pure position
|
|
@@ -446,9 +458,7 @@ export class Stack {
|
|
|
446
458
|
await assertNoParentCycle(this.readRecord, opts.id, opts.parentId);
|
|
447
459
|
}
|
|
448
460
|
const createdBy = normalizeActor(opts.createdBy);
|
|
449
|
-
const associations =
|
|
450
|
-
? stampGroupAdmin(opts.associations, createdBy?.subjectId ?? this.ownerEntityId)
|
|
451
|
-
: opts.associations;
|
|
461
|
+
const associations = createAssociations(typeId, opts.associations, createdBy, this.ownerEntityId);
|
|
452
462
|
// createdAt drives the id, so the two agree by construction rather than
|
|
453
463
|
// by coincidence. An explicit one mints via generateIdForTimestamp(),
|
|
454
464
|
// which never clamps to "now": generateId()'s monotonic floor would
|
|
@@ -508,230 +518,198 @@ export class Stack {
|
|
|
508
518
|
return opts.presentAt === 'latest' ? this.migrations.presentAtLatest(record) : record;
|
|
509
519
|
}
|
|
510
520
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* against the record's current stored type.
|
|
521
|
+
* Merge a patch into a record's content, checked against its current
|
|
522
|
+
* stored type. A patch the content already satisfies writes nothing.
|
|
514
523
|
* See docs/spec/data-model.md § Mutations.
|
|
515
524
|
*/
|
|
516
|
-
async
|
|
525
|
+
async patchContent(id, patch, opts = {}) {
|
|
517
526
|
this.assertOpen();
|
|
518
|
-
|
|
519
|
-
const existing = await this.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// Checked before validation, so a caller that lost the race learns its
|
|
523
|
-
// version is stale rather than that its patch is bad, and before the
|
|
524
|
-
// no-op short-circuit below, so a precondition is never satisfied by a
|
|
525
|
-
// write that turned out to move nothing.
|
|
526
|
-
if (takesIfVersion(changes))
|
|
527
|
-
this.checkIfVersion(existing, opts.ifVersion);
|
|
528
|
-
checkStoredVersion(existing, opts, changes.contentPatch !== undefined);
|
|
529
|
-
const merged = await this.validateChangeSet(id, existing, changes);
|
|
530
|
-
const ops = changeSetOps(existing, changes, merged);
|
|
531
|
-
// Nothing moved, so there is nothing to version. Decided here rather
|
|
532
|
-
// than in the adapter: "did this change anything" is a question about
|
|
533
|
-
// the record's meaning, and an adapter is never handed a write that
|
|
534
|
-
// writes nothing.
|
|
535
|
-
if (ops.length === 0)
|
|
527
|
+
assertContentPatch(patch);
|
|
528
|
+
const existing = await this.requireWritable(id, opts, true);
|
|
529
|
+
const merged = await this.validatePatch(existing, patch);
|
|
530
|
+
if (contentEqual(existing.content, merged))
|
|
536
531
|
return existing;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
// record stamp, since a non-bumping write never touches `updatedBy`.
|
|
541
|
-
// See docs/spec/versioning.md § Version history.
|
|
542
|
-
const bumps = bumpsVersion(ops);
|
|
543
|
-
// From the same before/after changeSetOps compared, so `ops` and the
|
|
544
|
-
// journal cannot disagree; ACL and data edits travel as one list.
|
|
545
|
-
// See docs/spec/events.md § The event shape.
|
|
546
|
-
const assocDelta = [
|
|
547
|
-
...(changes.associations
|
|
548
|
-
? associationDelta(existing.associations ?? [], changes.associations)
|
|
549
|
-
: []),
|
|
550
|
-
...(changes.permissions
|
|
551
|
-
? associationDelta(existing.permissions ?? [], changes.permissions)
|
|
552
|
-
: []),
|
|
553
|
-
];
|
|
554
|
-
const previousParentId = existing.parentId ?? null;
|
|
555
|
-
const change = new PendingChange(ops, {
|
|
556
|
-
actor: normalizeActor(opts.actor),
|
|
557
|
-
...(ops.includes('reparent') && { previousParentId }),
|
|
558
|
-
...(assocDelta.length && { associations: assocDelta }),
|
|
559
|
-
});
|
|
560
|
-
const updated = await this.adapter.mutateRecord(id, effectiveChanges(changes, ops), {
|
|
561
|
-
...(bumps ? this.writeOptions(existing, opts) : { actor: normalizeActor(opts.actor) }),
|
|
562
|
-
bumpsVersion: bumps,
|
|
532
|
+
const change = new PendingChange('patch', { actor: normalizeActor(opts.actor) });
|
|
533
|
+
const updated = await this.adapter.patchContent(id, patch, {
|
|
534
|
+
...this.writeOptions(existing, opts),
|
|
563
535
|
journal: change.journal,
|
|
564
536
|
});
|
|
565
537
|
this.announce(change, updated);
|
|
566
538
|
return updated;
|
|
567
539
|
}
|
|
568
540
|
/**
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
541
|
+
* Every check a content patch owes, all before anything is written.
|
|
542
|
+
* Returns the merged content, which the caller compares to decide whether
|
|
543
|
+
* content moved.
|
|
572
544
|
*/
|
|
573
|
-
async
|
|
574
|
-
|
|
545
|
+
async validatePatch(existing, patch) {
|
|
546
|
+
const patchErrors = [
|
|
547
|
+
...validateReservedKeys(patch),
|
|
548
|
+
...validatePatchValues(patch),
|
|
549
|
+
...validateContentKeys(patch),
|
|
550
|
+
];
|
|
551
|
+
if (patchErrors.length > 0)
|
|
552
|
+
throw new StackValidationError(patchErrors);
|
|
553
|
+
// The patch is what travels, so the patch is what's measured — a
|
|
554
|
+
// small patch against a large record is not an oversized request.
|
|
555
|
+
assertContentSize(patch, this.capabilities.limits.contentBytes, 'Patch');
|
|
556
|
+
const type = await this.getTypeCached(existing.typeId);
|
|
557
|
+
if (!type) {
|
|
558
|
+
throw new StackBadRequestError(`Unknown type: "${existing.typeId}"`);
|
|
559
|
+
}
|
|
560
|
+
const merged = applyMergePatch(existing.content, patch);
|
|
561
|
+
const contentErrors = typedContentErrors(existing.typeId, merged, type.schema);
|
|
562
|
+
if (contentErrors.length > 0)
|
|
563
|
+
throw new StackValidationError(contentErrors);
|
|
564
|
+
await checkContentWrite(this, existing.typeId, merged, existing);
|
|
565
|
+
return merged;
|
|
575
566
|
}
|
|
576
567
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
568
|
+
* Move a record under another, or to the root with `null`. The
|
|
569
|
+
* destination is checked only when it changes: it has to exist, and the
|
|
570
|
+
* move must not make the record its own ancestor.
|
|
571
|
+
* See docs/spec/data-model.md § Reparenting.
|
|
580
572
|
*/
|
|
581
|
-
async
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
assertDataAssociations(associations, 'associations');
|
|
590
|
-
if (permissions)
|
|
591
|
-
assertAuthorityAssociations(permissions, 'permissions');
|
|
592
|
-
const argumentErrors = [
|
|
593
|
-
...(permissions
|
|
594
|
-
? [...validateAssociations(permissions, 'permissions'), ...validatePermissions(permissions)]
|
|
595
|
-
: []),
|
|
596
|
-
...(associations ? validateAssociations(associations) : []),
|
|
597
|
-
];
|
|
598
|
-
const patchErrors = contentPatch
|
|
599
|
-
? [
|
|
600
|
-
...validateReservedKeys(contentPatch),
|
|
601
|
-
...validatePatchValues(contentPatch),
|
|
602
|
-
...validateContentKeys(contentPatch),
|
|
603
|
-
]
|
|
604
|
-
: [];
|
|
605
|
-
throwValidation(patchErrors, argumentErrors);
|
|
606
|
-
await checkAttachmentAssociationPointers(this.readRecord, associations, existing.associations);
|
|
607
|
-
let merged;
|
|
608
|
-
if (contentPatch) {
|
|
609
|
-
// The patch is what travels, so the patch is what's measured — a
|
|
610
|
-
// small patch against a large record is not an oversized request.
|
|
611
|
-
assertContentSize(contentPatch, this.capabilities.limits.contentBytes, 'Patch');
|
|
612
|
-
const type = await this.getTypeCached(existing.typeId);
|
|
613
|
-
if (!type) {
|
|
614
|
-
throw new StackBadRequestError(`Unknown type: "${existing.typeId}"`);
|
|
615
|
-
}
|
|
616
|
-
merged = applyMergePatch(existing.content, contentPatch);
|
|
617
|
-
const contentErrors = typedContentErrors(existing.typeId, merged, type.schema);
|
|
618
|
-
if (contentErrors.length > 0)
|
|
619
|
-
throw new StackValidationError(contentErrors);
|
|
620
|
-
if (existing.typeId === `${SYSTEM_TYPES.ATTACHMENT}@1`) {
|
|
621
|
-
// Presence decides mimeType and value decides the rest: re-sending
|
|
622
|
-
// the mimeType a record already holds is refused outright, while a
|
|
623
|
-
// client round-tripping fileId or size unchanged has claimed nothing.
|
|
624
|
-
assertAttachmentImmutable((field) => Object.prototype.hasOwnProperty.call(contentPatch, field) &&
|
|
625
|
-
(field === 'mimeType' ||
|
|
626
|
-
merged[field] !==
|
|
627
|
-
existing.content[field]));
|
|
628
|
-
}
|
|
629
|
-
await checkBindingsOnUpdate(this, existing.typeId, id, contentPatch, existing.content, merged);
|
|
630
|
-
if (id === SYSTEM_TYPES.CONFIG) {
|
|
631
|
-
checkConfigEntityIdUnchanged(existing.content.entityId, merged.entityId);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (associations !== undefined) {
|
|
635
|
-
assertGroupAdminRemains(existing, associations);
|
|
636
|
-
}
|
|
637
|
-
if (parentId !== undefined && parentId !== null && parentId !== (existing.parentId ?? null)) {
|
|
573
|
+
async setParent(id, parentId, opts = {}) {
|
|
574
|
+
this.assertOpen();
|
|
575
|
+
assertParentArgument(parentId);
|
|
576
|
+
const existing = await this.requireWritable(id, opts);
|
|
577
|
+
const previousParentId = existing.parentId ?? null;
|
|
578
|
+
if (parentId === previousParentId)
|
|
579
|
+
return existing;
|
|
580
|
+
if (parentId !== null) {
|
|
638
581
|
await assertParentExists(this.readRecord, id, parentId);
|
|
639
582
|
await assertNoParentCycle(this.readRecord, id, parentId);
|
|
640
583
|
}
|
|
641
|
-
|
|
584
|
+
const change = new PendingChange('reparent', {
|
|
585
|
+
actor: normalizeActor(opts.actor),
|
|
586
|
+
previousParentId,
|
|
587
|
+
});
|
|
588
|
+
return this.writeAspect(change, opts, (o) => this.adapter.setParent(id, parentId, o));
|
|
642
589
|
}
|
|
643
590
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* a reference came from.
|
|
591
|
+
* Withhold a record from enumeration, or restore it. Restating the
|
|
592
|
+
* current state writes nothing, so `unlistedAt` is never dragged forward.
|
|
593
|
+
* See docs/spec/unlisted.md.
|
|
648
594
|
*/
|
|
649
|
-
async
|
|
650
|
-
|
|
651
|
-
|
|
595
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
596
|
+
this.assertOpen();
|
|
597
|
+
assertUnlistedArgument(unlisted);
|
|
598
|
+
const existing = await this.requireWritable(id, opts);
|
|
599
|
+
if (Boolean(existing.unlistedAt) === unlisted)
|
|
600
|
+
return existing;
|
|
601
|
+
const change = new PendingChange(unlisted ? 'unlist' : 'list', {
|
|
602
|
+
actor: normalizeActor(opts.actor),
|
|
603
|
+
});
|
|
604
|
+
return this.writeAspect(change, opts, (o) => this.adapter.setUnlisted(id, unlisted, o));
|
|
652
605
|
}
|
|
653
606
|
/**
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
* `attachmentRecordId`
|
|
658
|
-
*
|
|
607
|
+
* Apply adds and removes to a record's data associations: one adapter
|
|
608
|
+
* write, one journal entry carrying each element in full, so the entry is
|
|
609
|
+
* as undoable as a single-element one. An add naming a held identity with
|
|
610
|
+
* a new `attachmentRecordId` re-points it — see docs/spec/attachments.md
|
|
611
|
+
* § Naming the upload a reference came from.
|
|
659
612
|
*/
|
|
660
|
-
async
|
|
661
|
-
|
|
662
|
-
|
|
613
|
+
async editAssociations(id, edits, opts = {}) {
|
|
614
|
+
this.assertOpen();
|
|
615
|
+
assertAssociationEdits(edits, 'editAssociations()', 'data');
|
|
616
|
+
return this.untilSetHolds(async () => {
|
|
617
|
+
const existing = await this.requireWritable(id, opts);
|
|
618
|
+
const next = applyAssociationEdits(existing.associations ?? [], edits);
|
|
619
|
+
return this.writeAssociations(existing, next, opts, (o) => this.adapter.editAssociations(id, edits, o));
|
|
620
|
+
});
|
|
663
621
|
}
|
|
664
622
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
* element in full — a remove with its annotation, a re-point with what it
|
|
668
|
-
* overwrote — so the entry is as undoable as a single-element one.
|
|
669
|
-
* See docs/spec/data-model.md § Mutations.
|
|
623
|
+
* Make a record's data associations exactly `associations`. Its
|
|
624
|
+
* permissions are another aspect, which this never reads or writes.
|
|
670
625
|
*/
|
|
671
|
-
async
|
|
626
|
+
async setAssociations(id, associations, opts = {}) {
|
|
672
627
|
this.assertOpen();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
628
|
+
assertAssociationSet(associations, 'associations');
|
|
629
|
+
return this.untilSetHolds(async () => {
|
|
630
|
+
const existing = await this.requireWritable(id, opts);
|
|
631
|
+
return this.writeAssociations(existing, associations, opts, (o) => this.adapter.setAssociations(id, associations, o));
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* The shared half of the two data-association writes, from the set the
|
|
636
|
+
* write would leave: nothing moved writes nothing, a roster keeps an
|
|
637
|
+
* admin, and every new attachment pointer names its own upload.
|
|
638
|
+
*/
|
|
639
|
+
async writeAssociations(existing, next, opts, write) {
|
|
677
640
|
const current = existing.associations ?? [];
|
|
678
|
-
const next = applyAssociationEdits(current, changes);
|
|
679
641
|
const delta = associationDelta(current, next);
|
|
680
642
|
if (delta.length === 0)
|
|
681
643
|
return existing;
|
|
682
|
-
// Only a roster can lose its last admin, and only the post-state says so.
|
|
683
644
|
assertGroupAdminRemains(existing, next);
|
|
684
|
-
await checkAttachmentAssociationPointers(this.readRecord,
|
|
645
|
+
await checkAttachmentAssociationPointers(this.readRecord, next, current);
|
|
685
646
|
const change = new PendingChange([
|
|
686
647
|
...(delta.some((c) => c.op !== 'remove') ? ['associate'] : []),
|
|
687
648
|
...(delta.some((c) => c.op === 'remove') ? ['dissociate'] : []),
|
|
688
649
|
], { actor: normalizeActor(opts.actor), associations: delta });
|
|
689
|
-
|
|
690
|
-
this.announce(change, updated);
|
|
691
|
-
return updated;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Extend who reaches a record. The spelling that survives two admins
|
|
695
|
-
* sharing a record at once: a `permissions` key write replaces the whole
|
|
696
|
-
* set, so the later of two concurrent ones drops what the earlier
|
|
697
|
-
* granted. No-bump, like associate(); an element the record already
|
|
698
|
-
* carries is a no-op. `write` never implies `read` — name both.
|
|
699
|
-
*/
|
|
700
|
-
async grantAccess(id, permissions, opts = {}) {
|
|
701
|
-
assertAssociationList(permissions, 'grantAccess()', 'permissions', 'authority');
|
|
702
|
-
return this.amendAccess(id, editsOf('add', permissions), opts, 'grantAccess()');
|
|
650
|
+
return this.writeAspect(change, opts, (o) => write({ ...o, expectSet: current }));
|
|
703
651
|
}
|
|
704
652
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
653
|
+
* Apply adds and removes to who reaches a record. The write-implies-read
|
|
654
|
+
* invariant is asked once, of the set the whole list produces, so a
|
|
655
|
+
* downgrade is one call. See docs/spec/access-control.md § Write implies read.
|
|
708
656
|
*/
|
|
709
|
-
async
|
|
710
|
-
|
|
711
|
-
|
|
657
|
+
async editAccess(id, edits, opts = {}) {
|
|
658
|
+
this.assertOpen();
|
|
659
|
+
assertAssociationEdits(edits, 'editAccess()', 'authority');
|
|
660
|
+
return this.untilSetHolds(async () => {
|
|
661
|
+
const existing = await this.requireWritable(id, opts);
|
|
662
|
+
const next = applyAssociationEdits(existing.permissions ?? [], edits);
|
|
663
|
+
return this.writeAccess(existing, next, opts, (o) => this.adapter.editAccess(id, edits, o));
|
|
664
|
+
});
|
|
712
665
|
}
|
|
713
|
-
/**
|
|
714
|
-
|
|
715
|
-
* write-implies-read invariant is asked once, of the set the whole list
|
|
716
|
-
* produces. See docs/spec/access-control.md § Write implies read.
|
|
717
|
-
*/
|
|
718
|
-
async amendAccess(id, changes, opts = {}, surface = 'amendAccess()') {
|
|
666
|
+
/** Make who reaches a record exactly `permissions`; `[]` is private. */
|
|
667
|
+
async setAccess(id, permissions, opts = {}) {
|
|
719
668
|
this.assertOpen();
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
669
|
+
assertAssociationSet(permissions, 'permissions');
|
|
670
|
+
return this.untilSetHolds(async () => {
|
|
671
|
+
const existing = await this.requireWritable(id, opts);
|
|
672
|
+
return this.writeAccess(existing, permissions, opts, (o) => this.adapter.setAccess(id, permissions, o));
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async writeAccess(existing, next, opts, write) {
|
|
724
676
|
const current = existing.permissions ?? [];
|
|
725
|
-
const next = applyAssociationEdits(current, changes);
|
|
726
677
|
const delta = associationDelta(current, next);
|
|
727
678
|
if (delta.length === 0)
|
|
728
679
|
return existing;
|
|
680
|
+
// After the no-op check: a call that moves nothing is not refused for
|
|
681
|
+
// a stored set it did not write.
|
|
729
682
|
this.assertPermissionSet(next);
|
|
730
683
|
const change = new PendingChange('reshare', {
|
|
731
684
|
actor: normalizeActor(opts.actor),
|
|
732
685
|
associations: delta,
|
|
733
686
|
});
|
|
734
|
-
|
|
687
|
+
return this.writeAspect(change, opts, (o) => write({ ...o, expectSet: current }));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Run an element-set write from its read onward, again from a fresh read
|
|
691
|
+
* each time the adapter finds the set moved since the last one, so the
|
|
692
|
+
* rules over the resulting set are asked of the set the write lands on.
|
|
693
|
+
* Unbounded: each refusal means another write landed.
|
|
694
|
+
* See docs/spec/versioning.md § Element-set writes compare the set they read.
|
|
695
|
+
*/
|
|
696
|
+
async untilSetHolds(attempt) {
|
|
697
|
+
for (;;) {
|
|
698
|
+
try {
|
|
699
|
+
return await attempt();
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
if (!(err instanceof ElementSetMovedError))
|
|
703
|
+
throw err;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Land a write that never bumps. Its precondition and journal entry
|
|
709
|
+
* travel together here, so no aspect verb can drop either.
|
|
710
|
+
*/
|
|
711
|
+
async writeAspect(change, opts, write) {
|
|
712
|
+
const updated = await write({ ifVersion: opts.ifVersion, journal: change.journal });
|
|
735
713
|
this.announce(change, updated);
|
|
736
714
|
return updated;
|
|
737
715
|
}
|
|
@@ -752,9 +730,24 @@ export class Stack {
|
|
|
752
730
|
* not applied to a tombstone.
|
|
753
731
|
*/
|
|
754
732
|
refuseIfDeleted(record) {
|
|
755
|
-
if (record.deletedAt)
|
|
756
|
-
throw
|
|
757
|
-
|
|
733
|
+
if (record.deletedAt)
|
|
734
|
+
throw softDeletedRefusal(record.id);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* The record a write addresses, past every refusal owed before
|
|
738
|
+
* validation: missing, another family, a caller's gate, soft-deleted,
|
|
739
|
+
* then a stale `ifVersion` — before validation, so a caller that lost the
|
|
740
|
+
* race hears that first, and before any no-op, so a write that moved
|
|
741
|
+
* nothing never satisfies a precondition.
|
|
742
|
+
*/
|
|
743
|
+
async requireWritable(id, opts, patchesContent = false) {
|
|
744
|
+
const existing = await this.requireRecord(id);
|
|
745
|
+
checkFamily(existing, opts);
|
|
746
|
+
await opts[WRITE_GATE]?.(existing);
|
|
747
|
+
this.refuseIfDeleted(existing);
|
|
748
|
+
this.checkIfVersion(existing, opts.ifVersion);
|
|
749
|
+
checkStoredVersion(existing, opts, patchesContent);
|
|
750
|
+
return existing;
|
|
758
751
|
}
|
|
759
752
|
/**
|
|
760
753
|
* A by-id read straight from the adapter, for the integrity checks a write
|
|
@@ -770,22 +763,13 @@ export class Stack {
|
|
|
770
763
|
return record;
|
|
771
764
|
}
|
|
772
765
|
/**
|
|
773
|
-
* Soft-delete a record, or purge it with its history
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* docs/spec/versioning.md § Deletion and docs/spec/attachments.md
|
|
777
|
-
* purge strands the bytes it referenced.
|
|
766
|
+
* Soft-delete a record, or purge it with its history, reporting the
|
|
767
|
+
* record as the write left it — or as a purge destroyed it — captured in
|
|
768
|
+
* the same write, so nothing lands unseen between a read and the delete.
|
|
769
|
+
* See docs/spec/versioning.md § Deletion and docs/spec/attachments.md
|
|
770
|
+
* § A purge strands the bytes it referenced.
|
|
778
771
|
*/
|
|
779
772
|
async delete(id, opts = {}) {
|
|
780
|
-
const { referencedFileIds } = await this.deleteAndReturn(id, opts);
|
|
781
|
-
return { referencedFileIds };
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* delete(), plus the record it acted on — before destruction for a purge,
|
|
785
|
-
* the tombstone for a soft delete — captured inside the same write, so no
|
|
786
|
-
* concurrent write can land unseen between a read and the delete.
|
|
787
|
-
*/
|
|
788
|
-
async deleteAndReturn(id, opts = {}) {
|
|
789
773
|
this.assertOpen();
|
|
790
774
|
if (id === SYSTEM_TYPES.CONFIG) {
|
|
791
775
|
throw new StackConflictError("Cannot delete the _config record: it holds the stack's identity and is required for every permission check.");
|
|
@@ -869,61 +853,27 @@ export class Stack {
|
|
|
869
853
|
return undeleted;
|
|
870
854
|
}
|
|
871
855
|
/**
|
|
872
|
-
* Query records. filter.baseId matches every version of a type family
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* docs/spec/data-model.md § Queries.
|
|
856
|
+
* Query records. filter.baseId matches every version of a type family.
|
|
857
|
+
* Results come back exactly as stored; pass presentAt: 'latest' to
|
|
858
|
+
* migrate in memory. See docs/spec/data-model.md § Queries.
|
|
876
859
|
*/
|
|
877
860
|
async query(query = {}) {
|
|
878
861
|
this.assertOpen();
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
assertValidSort(query.sort);
|
|
883
|
-
assertSortCapability(query.sort, this.adapter.capabilities);
|
|
884
|
-
assertValidAssociationFilters(filter);
|
|
885
|
-
assertValidBaseIdFilter(filter);
|
|
886
|
-
const limit = rawLimit !== undefined ? Math.min(rawLimit, MAX_QUERY_LIMIT) : undefined;
|
|
887
|
-
const resolvedFilter = await this.resolveBaseIdFilter(filter);
|
|
888
|
-
if (resolvedFilter === EMPTY_FAMILY) {
|
|
889
|
-
return { records: [], cursor: null };
|
|
890
|
-
}
|
|
891
|
-
const result = await this.adapter.queryRecords({
|
|
892
|
-
...rest,
|
|
893
|
-
...(query.sort && { sort: normalizeSort(query.sort) }),
|
|
894
|
-
...(resolvedFilter !== undefined && { filter: resolvedFilter }),
|
|
895
|
-
...(limit !== undefined && { limit }),
|
|
896
|
-
});
|
|
897
|
-
if (presentAt !== 'latest')
|
|
898
|
-
return result;
|
|
899
|
-
return { ...result, records: result.records.map((r) => this.migrations.presentAtLatest(r)) };
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Resolve filter.baseId into a concrete typeId set (intersected with
|
|
903
|
-
* filter.typeId when both are given), so adapters never need their own
|
|
904
|
-
* baseId concept. Returns EMPTY_FAMILY when the resolved set is empty so
|
|
905
|
-
* the caller can short-circuit without an adapter round trip.
|
|
906
|
-
*/
|
|
907
|
-
async resolveBaseIdFilter(filter) {
|
|
908
|
-
if (filter?.baseId === undefined)
|
|
909
|
-
return filter;
|
|
910
|
-
const { baseId, typeId, ...rest } = filter;
|
|
911
|
-
const requestedBaseIds = Array.isArray(baseId) ? baseId : [baseId];
|
|
912
|
-
const familyTypeIds = await this.familyTypeIds(requestedBaseIds);
|
|
913
|
-
const resolvedTypeIds = typeId === undefined
|
|
914
|
-
? familyTypeIds
|
|
915
|
-
: familyTypeIds.filter((id) => Array.isArray(typeId) ? typeId.includes(id) : typeId === id);
|
|
916
|
-
if (resolvedTypeIds.length === 0)
|
|
917
|
-
return EMPTY_FAMILY;
|
|
918
|
-
return { ...rest, typeId: resolvedTypeIds };
|
|
862
|
+
const split = splitQuery(query);
|
|
863
|
+
assertQueryCapabilities(split.query, this.adapter.capabilities);
|
|
864
|
+
return this.dispatchQuery(split, (q) => this.adapter.queryRecords(q));
|
|
919
865
|
}
|
|
920
866
|
/**
|
|
921
|
-
*
|
|
922
|
-
*
|
|
867
|
+
* Everything query() does around the adapter call once the query is
|
|
868
|
+
* split and checked: the migration on the way out. `read` is the call,
|
|
869
|
+
* so a scoped query within a reach gets the same treatment as any other.
|
|
923
870
|
*/
|
|
924
|
-
async
|
|
925
|
-
|
|
926
|
-
|
|
871
|
+
async dispatchQuery({ query, presentAt }, read) {
|
|
872
|
+
this.assertOpen();
|
|
873
|
+
const result = await read(query);
|
|
874
|
+
if (presentAt !== 'latest')
|
|
875
|
+
return result;
|
|
876
|
+
return { ...result, records: result.records.map((r) => this.migrations.presentAtLatest(r)) };
|
|
927
877
|
}
|
|
928
878
|
// -------------------------------------------------------
|
|
929
879
|
// Versions
|
|
@@ -975,17 +925,9 @@ export class Stack {
|
|
|
975
925
|
if (errors.length > 0) {
|
|
976
926
|
throw new StackValidationError(errors);
|
|
977
927
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
// Restoring is a write like any other, so it owes the same immutability
|
|
982
|
-
// check a content patch pays: a snapshot taken before a card claimed
|
|
983
|
-
// its DID would otherwise move the binding by rolling content back.
|
|
984
|
-
// Uniqueness needs none — a restore can only put back a value this same
|
|
985
|
-
// card already held.
|
|
986
|
-
for (const field of bindingFieldsOf(baseIdOf(target.typeId))) {
|
|
987
|
-
checkBindingImmutable(baseIdOf(target.typeId), field, existing.content[field], target.content[field]);
|
|
988
|
-
}
|
|
928
|
+
// Restoring is a write like any other, so it owes the checks every
|
|
929
|
+
// content write pays. See docs/spec/versioning.md § Restore semantics.
|
|
930
|
+
await checkContentWrite(this, target.typeId, target.content, existing);
|
|
989
931
|
// A restore adds no containment edge and takes none away, so there is
|
|
990
932
|
// no cycle for it to close and nothing for the destination checks to
|
|
991
933
|
// gate. See docs/spec/versioning.md § Restore semantics.
|
|
@@ -1035,7 +977,6 @@ export class Stack {
|
|
|
1035
977
|
assertContentSize(content, this.capabilities.limits.contentBytes, 'Content');
|
|
1036
978
|
const fromFamily = baseIdOf(existing.typeId);
|
|
1037
979
|
const toFamily = baseIdOf(toTypeId);
|
|
1038
|
-
const existingContent = existing.content;
|
|
1039
980
|
// Only create() stamps a group's first `admin`, so a record migrated
|
|
1040
981
|
// *into* `_group` would have an empty roster. Version-to-version within
|
|
1041
982
|
// the family keeps its roster. See docs/spec/data-model.md § Type migrations.
|
|
@@ -1043,21 +984,7 @@ export class Stack {
|
|
|
1043
984
|
throw new StackConflictError('Cannot migrate a record into _group: a group’s admin roster is stamped at creation. ' +
|
|
1044
985
|
'Create the group instead.');
|
|
1045
986
|
}
|
|
1046
|
-
|
|
1047
|
-
// Value-wise throughout: a migration re-sends all three required
|
|
1048
|
-
// fields, so presence would refuse every migration of the family.
|
|
1049
|
-
assertAttachmentImmutable((field) => content[field] !==
|
|
1050
|
-
existingContent[field]);
|
|
1051
|
-
}
|
|
1052
|
-
else if (toFamily === SYSTEM_TYPES.ATTACHMENT) {
|
|
1053
|
-
// A record arriving from outside the family stakes a fresh claim on
|
|
1054
|
-
// its fileId, exactly as create() does — so it owes create()'s check.
|
|
1055
|
-
await checkAttachmentMimeTypeOnCreate(this, content);
|
|
1056
|
-
}
|
|
1057
|
-
await checkBindingsOnMigrate(this, existing.typeId, toTypeId, id, existingContent, content);
|
|
1058
|
-
if (id === SYSTEM_TYPES.CONFIG) {
|
|
1059
|
-
checkConfigEntityIdUnchanged(existing.content.entityId, content.entityId);
|
|
1060
|
-
}
|
|
987
|
+
await checkContentWrite(this, toTypeId, content, existing);
|
|
1061
988
|
const change = new PendingChange('migrate', {
|
|
1062
989
|
actor: normalizeActor(opts.actor),
|
|
1063
990
|
previousTypeId: existing.typeId,
|
|
@@ -1086,8 +1013,8 @@ export class Stack {
|
|
|
1086
1013
|
// never sees it and this is the only place it can be announced.
|
|
1087
1014
|
const record = await this.adapter.putAttachmentWithMetadata(data, opts);
|
|
1088
1015
|
// The one emission with no journal half of its own: the far side
|
|
1089
|
-
// wrote the record, so it appended the entry in that same write
|
|
1090
|
-
// the
|
|
1016
|
+
// wrote the record, so it appended the entry in that same write: a
|
|
1017
|
+
// server is the only writer of its own durable tiers. See
|
|
1091
1018
|
// docs/spec/journal.md § The entry set is the event set.
|
|
1092
1019
|
this.announce(new PendingChange('create'), record);
|
|
1093
1020
|
return record;
|
|
@@ -1132,7 +1059,7 @@ export class Stack {
|
|
|
1132
1059
|
// Purged inside the adapter's own transaction, so these never reach
|
|
1133
1060
|
// delete() and are announced here instead — from the records it
|
|
1134
1061
|
// hands back, which are the last copies that will ever exist.
|
|
1135
|
-
deletedRecords = await this.adapter.deleteUnreferencedAttachmentRecords(fileId,
|
|
1062
|
+
deletedRecords = await this.adapter.deleteUnreferencedAttachmentRecords(fileId, SYSTEM_TYPES.ATTACHMENT);
|
|
1136
1063
|
const at = new Date();
|
|
1137
1064
|
for (const record of deletedRecords) {
|
|
1138
1065
|
this.announce(new PendingChange('purge', { actor: normalizeActor(opts.actor) }), record, at);
|
|
@@ -1203,7 +1130,6 @@ export class Stack {
|
|
|
1203
1130
|
...(opts.filter !== undefined && { filter: opts.filter }),
|
|
1204
1131
|
...(opts.since !== undefined && { since: opts.since }),
|
|
1205
1132
|
...(opts.includeRecords !== undefined && { includeRecords: opts.includeRecords }),
|
|
1206
|
-
...(opts.includeUnlisted !== undefined && { includeUnlisted: opts.includeUnlisted }),
|
|
1207
1133
|
...(opts.onError !== undefined && { onError: opts.onError }),
|
|
1208
1134
|
...(opts.onReset !== undefined && { onReset: opts.onReset }),
|
|
1209
1135
|
}, (change) => delivery.deliver(change));
|
|
@@ -1301,14 +1227,14 @@ export class Stack {
|
|
|
1301
1227
|
return apps.planInstall(this, submitted, opts);
|
|
1302
1228
|
}
|
|
1303
1229
|
/**
|
|
1304
|
-
* Apply an approved
|
|
1305
|
-
* Record and every linked key's grants
|
|
1306
|
-
*
|
|
1307
|
-
* See docs/spec/apps.md § Plan, then apply.
|
|
1230
|
+
* Apply an approved manifest: its types, the key's `_app` card, the
|
|
1231
|
+
* `_install` Record and every linked key's grants — refused with
|
|
1232
|
+
* StackConflictError unless it still plans to `opts.fingerprint`, the
|
|
1233
|
+
* plan the owner approved. See docs/spec/apps.md § Plan, then apply.
|
|
1308
1234
|
*/
|
|
1309
|
-
async installApp(
|
|
1235
|
+
async installApp(manifest, opts) {
|
|
1310
1236
|
this.assertOpen();
|
|
1311
|
-
return apps.installApp(this,
|
|
1237
|
+
return apps.installApp(this, manifest, opts);
|
|
1312
1238
|
}
|
|
1313
1239
|
/**
|
|
1314
1240
|
* Withdraw every grant an install produced and soft-delete it. The app's
|
|
@@ -1363,4 +1289,8 @@ export class Stack {
|
|
|
1363
1289
|
};
|
|
1364
1290
|
}
|
|
1365
1291
|
}
|
|
1292
|
+
/** Every registered version of the named families, as concrete TypeIds. */
|
|
1293
|
+
function typeIdsOfFamilies(types, baseIds) {
|
|
1294
|
+
return types.filter((t) => baseIds.includes(t.baseId)).map((t) => t.id);
|
|
1295
|
+
}
|
|
1366
1296
|
//# sourceMappingURL=stack.js.map
|