@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/README.md
CHANGED
|
@@ -34,7 +34,6 @@ const keyPath = './my-stack.key.json'; // see "Key custody" under Identity for w
|
|
|
34
34
|
const adapter = await LocalAdapter.open({
|
|
35
35
|
path: dbPath,
|
|
36
36
|
create: 'ifMissing',
|
|
37
|
-
timezone: 'America/New_York',
|
|
38
37
|
ownerEntityId: async () => {
|
|
39
38
|
const { did, privateKey } = await generateDidKeypair();
|
|
40
39
|
await writeFile(keyPath, JSON.stringify(await exportDidPrivateKeyJwk(privateKey)));
|
|
@@ -74,14 +73,12 @@ await notes.patchContent(note.id, { title: 'Updated title' });
|
|
|
74
73
|
const same = await notes.get(note.id);
|
|
75
74
|
|
|
76
75
|
// Tag it
|
|
77
|
-
await notes.
|
|
76
|
+
await notes.editAssociations(note.id, [
|
|
77
|
+
{ op: 'add', association: { kind: 'tag', label: 'favourite' } },
|
|
78
|
+
]);
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
-
await notes.
|
|
81
|
-
contentPatch: { title: 'Final title' },
|
|
82
|
-
permissions: [{ kind: 'anyone', label: 'read' }],
|
|
83
|
-
unlisted: false,
|
|
84
|
-
});
|
|
80
|
+
// Share it with everyone — each aspect of a record has its own verb
|
|
81
|
+
await notes.setAccess(note.id, [{ kind: 'anyone', label: 'read' }]);
|
|
85
82
|
|
|
86
83
|
// Query the family. stack.query() reads across every type you can see instead.
|
|
87
84
|
const { records: favourites } = await notes.query({
|
|
@@ -206,7 +203,7 @@ export const manifest: AppManifest = {
|
|
|
206
203
|
|
|
207
204
|
**When the owner runs the app**, it opens with `Stack.open(adapter, { migrations })`, calls `install(stack)`, then `new Library(stack)`. A server hands each requester `new Library(stack.asActor(session))`.
|
|
208
205
|
|
|
209
|
-
**When the app runs elsewhere** — holding its own key and reaching the stack through `APIAdapter` — it can't define types, so it asks: `adapter.requestInstall(manifest)` resolves to `pending` until the owner approves. The owner reviews `stack.planInstall(manifest, { did })` and applies it with `stack.installApp(plan)`, which defines the types, registers the app's key and grants it the requests. The app still opens with its `migrations`, then builds `new Library(stack)` over its own connection. See [App installs](https://github.com/haverstack/core/blob/main/docs/spec/apps.md).
|
|
206
|
+
**When the app runs elsewhere** — holding its own key and reaching the stack through `APIAdapter` — it can't define types, so it asks: `adapter.requestInstall(manifest)` resolves to `pending` until the owner approves. The owner reviews `stack.planInstall(manifest, { did })` and applies it with `stack.installApp(manifest, { did, fingerprint: plan.fingerprint })`, which defines the types, registers the app's key and grants it the requests. The app still opens with its `migrations`, then builds `new Library(stack)` over its own connection. See [App installs](https://github.com/haverstack/core/blob/main/docs/spec/apps.md).
|
|
210
207
|
|
|
211
208
|
**Name content with an interface**, as above. `ContentOf` flattens its result, so a `type` alias shows the expanded object in editor hovers, while an interface keeps its name. typescript-eslint's recommended `no-empty-object-type` rule rejects an empty interface; its `allowInterfaces: 'with-single-extends'` option admits this one.
|
|
212
209
|
|
package/dist/access.d.ts
CHANGED
|
@@ -1,47 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stack — Permission Resolution
|
|
3
3
|
* -------------------------------------------------------
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and the grant path resolve against, and of the owner-acting-alone tier.
|
|
4
|
+
* The record-level permission rules, with no dependency on a transport
|
|
5
|
+
* layer or storage backend: how a grantee reaches an entity, the
|
|
6
|
+
* write-implies-read invariant, and the `_group` roster rules both layers
|
|
7
|
+
* resolve against, plus the owner-acting-alone tier. What a requester may
|
|
8
|
+
* do is a reach per verb, which scoped-stack/authority.ts builds from these.
|
|
10
9
|
*
|
|
11
10
|
* A permission set is read as data throughout, never as the type: it can
|
|
12
11
|
* arrive from an import or a foreign server, where no compiler has seen
|
|
13
12
|
* it. Every tier is affirmative, so an element read past its own kind —
|
|
14
13
|
* or carrying no kind at all — confers nothing.
|
|
15
14
|
*/
|
|
16
|
-
import type { AuthorityAssociation, DataAssociation, EntityId, GroupRole, RecordId, StackRecord } from './types/index.js';
|
|
15
|
+
import type { Actor, AuthorityAssociation, DataAssociation, EntityId, GroupRole, Grantee, RecordId, StackRecord, TypeId } from './types/index.js';
|
|
17
16
|
import type { ValidationError } from './validate.js';
|
|
18
|
-
|
|
17
|
+
/** Resolves a Record by ID, for walks that need no full adapter. */
|
|
18
|
+
export type RecordResolver = (id: RecordId) => Promise<StackRecord | null>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* The roles one entity holds on live `_group` rosters, keyed by group id,
|
|
21
|
+
* loaded on first use and at most once per operation, so no resolved role
|
|
22
|
+
* outlives the operation that read it — removal from a group must never go
|
|
23
|
+
* stale. See loadRosterRoles().
|
|
22
24
|
*/
|
|
23
|
-
export type
|
|
25
|
+
export type RosterRoles = () => Promise<ReadonlyMap<RecordId, GroupRole>>;
|
|
26
|
+
/**
|
|
27
|
+
* The elements beside which a `write` naming `grantee` carries its bit: its
|
|
28
|
+
* own `read`, or an `anyone`, which reaches the grantee already. The one
|
|
29
|
+
* statement of write-implies-read — validation and every reach ask it.
|
|
30
|
+
* See docs/spec/access-control.md § Write implies read.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readCompanions(grantee: Grantee): AuthorityAssociation[];
|
|
24
33
|
/**
|
|
25
|
-
* Whether
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* principal. See docs/spec/access-control.md § Delegation: principal and subject.
|
|
34
|
+
* Whether a grantee reaches `entityId`, for both layers alike. Read as data,
|
|
35
|
+
* so an arm the write would refuse confers nothing; `member` is the wider
|
|
36
|
+
* role. See docs/spec/access-control.md § Refused at the write, and again at evaluation.
|
|
29
37
|
*/
|
|
30
|
-
export declare function
|
|
38
|
+
export declare function granteeReaches(grantee: unknown, entityId: EntityId, opts: {
|
|
39
|
+
roles: RosterRoles;
|
|
40
|
+
allowGroup?: boolean;
|
|
41
|
+
}): Promise<boolean>;
|
|
31
42
|
/**
|
|
32
|
-
* Rejects a permission set where some
|
|
33
|
-
*
|
|
43
|
+
* Rejects a permission set where some `write` stands without a
|
|
44
|
+
* readCompanions() element, which no reach would honor. A cross-element
|
|
34
45
|
* invariant, so it is asked of the set the write would *produce*, the same
|
|
35
46
|
* way hasGroupAdmin() reads its post-state below.
|
|
36
47
|
* See docs/spec/access-control.md § Write implies read.
|
|
37
48
|
*/
|
|
38
49
|
export declare function validatePermissions(permissions: AuthorityAssociation[] | undefined, path?: string): ValidationError[];
|
|
39
|
-
/**
|
|
40
|
-
* An entity's role on the `_group` roster `groupId` names. `memo`, when
|
|
41
|
-
* given, is built per operation, so no resolved role outlives the operation
|
|
42
|
-
* that resolved it — removal from a group must never go stale.
|
|
43
|
-
*/
|
|
44
|
-
export declare function resolveGroupRole(groupId: RecordId, entityId: EntityId, resolveRecord: RecordResolver, memo?: Map<string, GroupRole | null>): Promise<GroupRole | null>;
|
|
45
50
|
/** Whether a held role meets a grantee's: an admin is also a member. */
|
|
46
51
|
export declare function roleSatisfies(held: GroupRole | null, required: GroupRole): boolean;
|
|
47
52
|
/**
|
|
@@ -49,17 +54,10 @@ export declare function roleSatisfies(held: GroupRole | null, required: GroupRol
|
|
|
49
54
|
* whose roster rules apply.
|
|
50
55
|
*/
|
|
51
56
|
export declare function isGroupRecord(record: StackRecord): boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Whether a Record is a roster anything may resolve against: a `_group`,
|
|
54
|
-
* or an app's own `member` links would turn records into ACLs, and not
|
|
55
|
-
* deleted, since deleting a Group withdraws it. Shared with the grant path
|
|
56
|
-
* so the two cannot disagree. See docs/spec/identity.md § Group.
|
|
57
|
-
*/
|
|
58
|
-
export declare function carriesRoster(record: StackRecord): boolean;
|
|
59
57
|
/**
|
|
60
58
|
* An entity's role on a `_group` roster; a matching `admin` wins regardless
|
|
61
59
|
* of order. Reads associations alone and cannot tell whose they are, so
|
|
62
|
-
* callers
|
|
60
|
+
* callers ask it of a live `_group` only — see loadRosterRoles().
|
|
63
61
|
*/
|
|
64
62
|
export declare function groupRoleFromAssociations(associations: DataAssociation[] | undefined, entityId: EntityId): GroupRole | null;
|
|
65
63
|
/**
|
|
@@ -69,8 +67,12 @@ export declare function groupRoleFromAssociations(associations: DataAssociation[
|
|
|
69
67
|
* the last. Reads associations alone, like groupRoleFromAssociations().
|
|
70
68
|
*/
|
|
71
69
|
export declare function hasGroupAdmin(associations: DataAssociation[] | undefined): boolean;
|
|
72
|
-
/**
|
|
73
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The associations a create stores: as given, plus a `_group`'s creator —
|
|
72
|
+
* the owner when none is named — stamped as its first admin.
|
|
73
|
+
* See docs/spec/identity.md § Group.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createAssociations(typeId: TypeId, associations: DataAssociation[] | undefined, createdBy: Actor | undefined, ownerEntityId: EntityId): DataAssociation[] | undefined;
|
|
74
76
|
/**
|
|
75
77
|
* Whether a request is the stack owner acting alone — the tier behind every
|
|
76
78
|
* unconditional owner authority. Being the owner is never sufficient on its
|
package/dist/access.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EACV,KAAK,EAEL,oBAAoB,EACpB,eAAe,EACf,QAAQ,EAER,SAAS,EACT,OAAO,EACP,QAAQ,EAER,WAAW,EACX,MAAM,EACP,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,oEAAoE;AACpE,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AAE1E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,oBAAoB,EAAE,CAKvE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD,OAAO,CAAC,OAAO,CAAC,CAelB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,oBAAoB,EAAE,GAAG,SAAS,EAC/C,IAAI,SAAgB,GACnB,eAAe,EAAE,CAcnB;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,QAAQ,EAAE,SAAS,GAAG,OAAO,CAElF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,eAAe,EAAE,GAAG,SAAS,EAC3C,QAAQ,EAAE,QAAQ,GACjB,SAAS,GAAG,IAAI,CASlB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,SAAS,GAAG,OAAO,CAElF;AAkBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,eAAe,EAAE,GAAG,SAAS,EAC3C,SAAS,EAAE,KAAK,GAAG,SAAS,EAC5B,aAAa,EAAE,QAAQ,GACtB,eAAe,EAAE,GAAG,SAAS,CAG/B;AAiBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IAAE,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;CAAE,EACpE,aAAa,EAAE,QAAQ,GACtB,OAAO,CAGT"}
|
package/dist/access.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stack — Permission Resolution
|
|
3
3
|
* -------------------------------------------------------
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and the grant path resolve against, and of the owner-acting-alone tier.
|
|
4
|
+
* The record-level permission rules, with no dependency on a transport
|
|
5
|
+
* layer or storage backend: how a grantee reaches an entity, the
|
|
6
|
+
* write-implies-read invariant, and the `_group` roster rules both layers
|
|
7
|
+
* resolve against, plus the owner-acting-alone tier. What a requester may
|
|
8
|
+
* do is a reach per verb, which scoped-stack/authority.ts builds from these.
|
|
10
9
|
*
|
|
11
10
|
* A permission set is read as data throughout, never as the type: it can
|
|
12
11
|
* arrive from an import or a foreign server, where no compiler has seen
|
|
@@ -14,115 +13,67 @@
|
|
|
14
13
|
* or carrying no kind at all — confers nothing.
|
|
15
14
|
*/
|
|
16
15
|
import { SYSTEM_TYPES } from './types/index.js';
|
|
17
|
-
import {
|
|
16
|
+
import { associationKey } from './associations/identity.js';
|
|
18
17
|
import { baseIdOf } from './schema.js';
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* principal. See docs/spec/access-control.md § Delegation: principal and subject.
|
|
24
|
-
*/
|
|
25
|
-
export async function checkAccess(record, subjectEntityId, ownerEntityId, mode, resolveRecord) {
|
|
26
|
-
if (subjectEntityId && subjectEntityId === ownerEntityId)
|
|
27
|
-
return true;
|
|
28
|
-
const perms = record.permissions;
|
|
29
|
-
// No permissions = private.
|
|
30
|
-
if (!perms || perms.length === 0)
|
|
31
|
-
return false;
|
|
32
|
-
// Two elements can name one group (a `member` and an `admin` read), and
|
|
33
|
-
// the roster answers both the same way within one check.
|
|
34
|
-
const roles = new Map();
|
|
35
|
-
for (const p of perms) {
|
|
36
|
-
if (p?.kind === 'anyone') {
|
|
37
|
-
if (mode === 'read' && isWorldRead(p))
|
|
38
|
-
return true;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (p?.kind !== 'permission' || !p.grantee)
|
|
42
|
-
continue;
|
|
43
|
-
if (p.label !== mode)
|
|
44
|
-
continue;
|
|
45
|
-
// The write bit is inert without read alongside it: the mutate surface
|
|
46
|
-
// hands back the record it wrote and opens its whole history, so a
|
|
47
|
-
// write element standing alone would withhold nothing. Refused at the
|
|
48
|
-
// write by validatePermissions() and again here.
|
|
49
|
-
// See docs/spec/access-control.md § Write implies read.
|
|
50
|
-
if (mode === 'write' && !holdsRead(perms, p.grantee))
|
|
51
|
-
continue;
|
|
52
|
-
if (await granteeCovers(p.grantee, subjectEntityId, resolveRecord, roles))
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Whether the set conveys read to this grantee — a `read` of its own, or
|
|
59
|
-
* an `anyone` that already reaches everyone. Demanding a second, redundant
|
|
60
|
-
* `read` beside the write would refuse a set that withholds nothing;
|
|
61
|
-
* revoking the `anyone` is what the invariant catches instead.
|
|
19
|
+
* The elements beside which a `write` naming `grantee` carries its bit: its
|
|
20
|
+
* own `read`, or an `anyone`, which reaches the grantee already. The one
|
|
21
|
+
* statement of write-implies-read — validation and every reach ask it.
|
|
62
22
|
* See docs/spec/access-control.md § Write implies read.
|
|
63
23
|
*/
|
|
64
|
-
function
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Whether an `anyone` element reaches the world. `read` is the only bit it
|
|
71
|
-
* can carry, so one labelled otherwise names no reach — refused at the
|
|
72
|
-
* write by validateAssociation() and read as nothing here.
|
|
73
|
-
*/
|
|
74
|
-
function isWorldRead(association) {
|
|
75
|
-
return association.label === 'read';
|
|
24
|
+
export function readCompanions(grantee) {
|
|
25
|
+
return [
|
|
26
|
+
{ kind: 'permission', label: 'read', grantee },
|
|
27
|
+
{ kind: 'anyone', label: 'read' },
|
|
28
|
+
];
|
|
76
29
|
}
|
|
77
30
|
/**
|
|
78
|
-
* Whether a grantee reaches
|
|
79
|
-
*
|
|
31
|
+
* Whether a grantee reaches `entityId`, for both layers alike. Read as data,
|
|
32
|
+
* so an arm the write would refuse confers nothing; `member` is the wider
|
|
33
|
+
* role. See docs/spec/access-control.md § Refused at the write, and again at evaluation.
|
|
80
34
|
*/
|
|
81
|
-
async function
|
|
82
|
-
if (!
|
|
35
|
+
export async function granteeReaches(grantee, entityId, opts) {
|
|
36
|
+
if (!grantee || typeof grantee !== 'object')
|
|
83
37
|
return false;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
38
|
+
const g = grantee;
|
|
39
|
+
switch (g.kind) {
|
|
40
|
+
case 'entity':
|
|
41
|
+
return !!g.entityId && g.entityId === entityId;
|
|
42
|
+
case 'group': {
|
|
43
|
+
if (opts.allowGroup === false || !g.groupId)
|
|
44
|
+
return false;
|
|
45
|
+
if (g.role !== 'member' && g.role !== 'admin')
|
|
46
|
+
return false;
|
|
47
|
+
const held = (await opts.roles()).get(g.groupId) ?? null;
|
|
48
|
+
return roleSatisfies(held, g.role);
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
88
53
|
}
|
|
89
54
|
/**
|
|
90
|
-
* Rejects a permission set where some
|
|
91
|
-
*
|
|
55
|
+
* Rejects a permission set where some `write` stands without a
|
|
56
|
+
* readCompanions() element, which no reach would honor. A cross-element
|
|
92
57
|
* invariant, so it is asked of the set the write would *produce*, the same
|
|
93
58
|
* way hasGroupAdmin() reads its post-state below.
|
|
94
59
|
* See docs/spec/access-control.md § Write implies read.
|
|
95
60
|
*/
|
|
96
61
|
export function validatePermissions(permissions, path = 'permissions') {
|
|
97
62
|
const list = permissions ?? [];
|
|
63
|
+
const held = new Set(list.map(associationKey));
|
|
98
64
|
const errors = [];
|
|
99
65
|
list.forEach((p, i) => {
|
|
100
66
|
if (p?.kind !== 'permission' || p.label !== 'write')
|
|
101
67
|
return;
|
|
102
|
-
if (
|
|
68
|
+
if (readCompanions(p.grantee).some((c) => held.has(associationKey(c))))
|
|
103
69
|
return;
|
|
104
70
|
errors.push({
|
|
105
71
|
path: `${path}[${i}]`,
|
|
106
|
-
message: 'write requires read: a write-holder reaches the record and its history through the mutate surface, so a `write` element with no `read` for the same grantee withholds nothing. Grant `read` and `write` together
|
|
72
|
+
message: 'write requires read: a write-holder reaches the record and its history through the mutate surface, so a `write` element with no `read` for the same grantee withholds nothing. Grant `read` and `write` together in one editAccess() or setAccess() call',
|
|
107
73
|
});
|
|
108
74
|
});
|
|
109
75
|
return errors;
|
|
110
76
|
}
|
|
111
|
-
/**
|
|
112
|
-
* An entity's role on the `_group` roster `groupId` names. `memo`, when
|
|
113
|
-
* given, is built per operation, so no resolved role outlives the operation
|
|
114
|
-
* that resolved it — removal from a group must never go stale.
|
|
115
|
-
*/
|
|
116
|
-
export async function resolveGroupRole(groupId, entityId, resolveRecord, memo) {
|
|
117
|
-
const key = `${groupId}:${entityId}`;
|
|
118
|
-
const cached = memo?.get(key);
|
|
119
|
-
if (cached !== undefined)
|
|
120
|
-
return cached;
|
|
121
|
-
const group = await resolveRecord(groupId);
|
|
122
|
-
const role = group && carriesRoster(group) ? groupRoleFromAssociations(group.associations, entityId) : null;
|
|
123
|
-
memo?.set(key, role);
|
|
124
|
-
return role;
|
|
125
|
-
}
|
|
126
77
|
/** Whether a held role meets a grantee's: an admin is also a member. */
|
|
127
78
|
export function roleSatisfies(held, required) {
|
|
128
79
|
return required === 'admin' ? held === 'admin' : held !== null;
|
|
@@ -134,19 +85,10 @@ export function roleSatisfies(held, required) {
|
|
|
134
85
|
export function isGroupRecord(record) {
|
|
135
86
|
return baseIdOf(record.typeId) === SYSTEM_TYPES.GROUP;
|
|
136
87
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Whether a Record is a roster anything may resolve against: a `_group`,
|
|
139
|
-
* or an app's own `member` links would turn records into ACLs, and not
|
|
140
|
-
* deleted, since deleting a Group withdraws it. Shared with the grant path
|
|
141
|
-
* so the two cannot disagree. See docs/spec/identity.md § Group.
|
|
142
|
-
*/
|
|
143
|
-
export function carriesRoster(record) {
|
|
144
|
-
return isGroupRecord(record) && !record.deletedAt;
|
|
145
|
-
}
|
|
146
88
|
/**
|
|
147
89
|
* An entity's role on a `_group` roster; a matching `admin` wins regardless
|
|
148
90
|
* of order. Reads associations alone and cannot tell whose they are, so
|
|
149
|
-
* callers
|
|
91
|
+
* callers ask it of a live `_group` only — see loadRosterRoles().
|
|
150
92
|
*/
|
|
151
93
|
export function groupRoleFromAssociations(associations, entityId) {
|
|
152
94
|
let role = null;
|
|
@@ -180,8 +122,17 @@ function isGroupAdminAssociation(association) {
|
|
|
180
122
|
association.label === 'admin' &&
|
|
181
123
|
association.target.kind === 'entity');
|
|
182
124
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The associations a create stores: as given, plus a `_group`'s creator —
|
|
127
|
+
* the owner when none is named — stamped as its first admin.
|
|
128
|
+
* See docs/spec/identity.md § Group.
|
|
129
|
+
*/
|
|
130
|
+
export function createAssociations(typeId, associations, createdBy, ownerEntityId) {
|
|
131
|
+
if (baseIdOf(typeId) !== SYSTEM_TYPES.GROUP)
|
|
132
|
+
return associations;
|
|
133
|
+
return stampGroupAdmin(associations, createdBy?.subjectId ?? ownerEntityId);
|
|
134
|
+
}
|
|
135
|
+
function stampGroupAdmin(associations, creator) {
|
|
185
136
|
const list = associations ?? [];
|
|
186
137
|
const alreadyAdmin = list.some((a) => isGroupAdminAssociation(a) && a.target.entityId === creator);
|
|
187
138
|
if (alreadyAdmin)
|
package/dist/access.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combine.d.ts","sourceRoot":"","sources":["../../src/adapter/combine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"combine.d.ts","sourceRoot":"","sources":["../../src/adapter/combine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAKV,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,YAAY,CA8Ef"}
|
package/dist/adapter/combine.js
CHANGED
|
@@ -20,19 +20,20 @@ export function combineAdapters(parts) {
|
|
|
20
20
|
get ownerEntityId() {
|
|
21
21
|
return parts.record.ownerEntityId;
|
|
22
22
|
},
|
|
23
|
-
get timezone() {
|
|
24
|
-
return parts.record.timezone;
|
|
25
|
-
},
|
|
26
23
|
createRecord: (r, opts) => parts.record.createRecord(r, opts),
|
|
27
24
|
getRecord: (id) => parts.record.getRecord(id),
|
|
28
|
-
|
|
25
|
+
patchContent: (id, patch, opts) => parts.record.patchContent(id, patch, opts),
|
|
29
26
|
deleteRecord: (id, opts) => parts.record.deleteRecord(id, opts),
|
|
30
27
|
undeleteRecord: (id, opts) => parts.record.undeleteRecord(id, opts),
|
|
31
28
|
queryRecords: (q) => parts.record.queryRecords(q),
|
|
32
|
-
|
|
29
|
+
setParent: (id, parentId, opts) => parts.record.setParent(id, parentId, opts),
|
|
30
|
+
setUnlisted: (id, unlisted, opts) => parts.record.setUnlisted(id, unlisted, opts),
|
|
31
|
+
editAssociations: (id, edits, opts) => parts.record.editAssociations(id, edits, opts),
|
|
32
|
+
setAssociations: (id, list, opts) => parts.record.setAssociations(id, list, opts),
|
|
33
|
+
editAccess: (id, edits, opts) => parts.record.editAccess(id, edits, opts),
|
|
34
|
+
setAccess: (id, list, opts) => parts.record.setAccess(id, list, opts),
|
|
33
35
|
getVersions: (id, query) => parts.record.getVersions(id, query),
|
|
34
36
|
getVersion: (id, v) => parts.record.getVersion(id, v),
|
|
35
|
-
saveVersion: (id, v) => parts.record.saveVersion(id, v),
|
|
36
37
|
getJournal: (id, query) => parts.record.getJournal(id, query),
|
|
37
38
|
restoreVersion: (id, v, opts) => parts.record.restoreVersion(id, v, opts),
|
|
38
39
|
commitMigration: (id, toTypeId, content, opts) => parts.record.commitMigration(id, toTypeId, content, opts),
|
|
@@ -43,7 +44,13 @@ export function combineAdapters(parts) {
|
|
|
43
44
|
// absent on the combined adapter, rather than silently becoming
|
|
44
45
|
// "supported" via a wrapper that forwards to a missing method.
|
|
45
46
|
...(parts.record.deleteUnreferencedAttachmentRecords && {
|
|
46
|
-
deleteUnreferencedAttachmentRecords: (fileId,
|
|
47
|
+
deleteUnreferencedAttachmentRecords: (fileId, metadataBaseId) => parts.record.deleteUnreferencedAttachmentRecords(fileId, metadataBaseId),
|
|
48
|
+
}),
|
|
49
|
+
...(parts.record.importRecord && {
|
|
50
|
+
importRecord: (r, history) => parts.record.importRecord(r, history),
|
|
51
|
+
}),
|
|
52
|
+
...(parts.record.queryReadable && {
|
|
53
|
+
queryReadable: (q, reach) => parts.record.queryReadable(q, reach),
|
|
47
54
|
}),
|
|
48
55
|
// Forwarded on the same terms, and it decides more than it looks:
|
|
49
56
|
// Stack.relaysChanges reads its presence, so dropping it here would
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combine.js","sourceRoot":"","sources":["../../src/adapter/combine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"combine.js","sourceRoot":"","sources":["../../src/adapter/combine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copying a stack
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* One stack, read in full from one adapter and written in full to a new
|
|
5
|
+
* store: every Record (soft-deleted and unlisted included) with its version
|
|
6
|
+
* snapshots and journal, `_config` among them as the seed the store is
|
|
7
|
+
* created from, every Type, and every blob. Each is read as the store
|
|
8
|
+
* enumerates it, never by what a Record says, so nothing is filtered on the
|
|
9
|
+
* way. Export, import and moving
|
|
10
|
+
* between storage engines are all this one call, with a different source
|
|
11
|
+
* and a different kind of store to create. Full trust, like any direct
|
|
12
|
+
* adapter access: authorship and history land as recorded, which no write
|
|
13
|
+
* through Stack may do. See docs/spec/portability.md.
|
|
14
|
+
*/
|
|
15
|
+
import type { StackAdapter, StoreSeed } from '../types/index.js';
|
|
16
|
+
/** Creates a new store from `seed`: `LocalAdapter.open({ path, seed })`, say. */
|
|
17
|
+
export type CreateStore = (seed: StoreSeed) => Promise<StackAdapter> | StackAdapter;
|
|
18
|
+
/**
|
|
19
|
+
* Copy everything `from` holds into the store `create` makes from its
|
|
20
|
+
* `_config` record and that record's history, and return that store, open.
|
|
21
|
+
* Not atomic: a failure closes the store part-written, and the caller, whose
|
|
22
|
+
* store it is, removes it. The source must not be written meanwhile; nothing
|
|
23
|
+
* here can check that.
|
|
24
|
+
* See docs/spec/portability.md § Copying a stack.
|
|
25
|
+
*/
|
|
26
|
+
export declare function copyStack(from: StackAdapter, create: CreateStore): Promise<StackAdapter>;
|
|
27
|
+
//# sourceMappingURL=copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy.d.ts","sourceRoot":"","sources":["../../src/adapter/copy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAGV,YAAY,EAEZ,SAAS,EACV,MAAM,mBAAmB,CAAC;AAG3B,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;AAYpF;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAgC9F"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copying a stack
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* One stack, read in full from one adapter and written in full to a new
|
|
5
|
+
* store: every Record (soft-deleted and unlisted included) with its version
|
|
6
|
+
* snapshots and journal, `_config` among them as the seed the store is
|
|
7
|
+
* created from, every Type, and every blob. Each is read as the store
|
|
8
|
+
* enumerates it, never by what a Record says, so nothing is filtered on the
|
|
9
|
+
* way. Export, import and moving
|
|
10
|
+
* between storage engines are all this one call, with a different source
|
|
11
|
+
* and a different kind of store to create. Full trust, like any direct
|
|
12
|
+
* adapter access: authorship and history land as recorded, which no write
|
|
13
|
+
* through Stack may do. See docs/spec/portability.md.
|
|
14
|
+
*/
|
|
15
|
+
import { InvalidAdapterError, OwnerMismatchError } from '../errors.js';
|
|
16
|
+
import { resolveQuery } from '../query-validation.js';
|
|
17
|
+
import { SYSTEM_TYPES } from '../types/index.js';
|
|
18
|
+
/** Records read per page; any size works, as each page is written before the next is read. */
|
|
19
|
+
const PAGE_SIZE = 100;
|
|
20
|
+
/** Every Record a store holds but `_config`, which no query reaches: a store is created from it. */
|
|
21
|
+
const EVERYTHING = {
|
|
22
|
+
filter: { includeDeleted: true, includeUnlisted: true },
|
|
23
|
+
sort: { field: 'createdAt', direction: 'asc' },
|
|
24
|
+
limit: PAGE_SIZE,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Copy everything `from` holds into the store `create` makes from its
|
|
28
|
+
* `_config` record and that record's history, and return that store, open.
|
|
29
|
+
* Not atomic: a failure closes the store part-written, and the caller, whose
|
|
30
|
+
* store it is, removes it. The source must not be written meanwhile; nothing
|
|
31
|
+
* here can check that.
|
|
32
|
+
* See docs/spec/portability.md § Copying a stack.
|
|
33
|
+
*/
|
|
34
|
+
export async function copyStack(from, create) {
|
|
35
|
+
const listBlobs = from.listBlobs?.bind(from);
|
|
36
|
+
if (!listBlobs) {
|
|
37
|
+
throw new InvalidAdapterError('Cannot copy from this adapter: it has no listBlobs(), so its blobs cannot be enumerated.');
|
|
38
|
+
}
|
|
39
|
+
const config = await from.getRecord(SYSTEM_TYPES.CONFIG);
|
|
40
|
+
if (!config) {
|
|
41
|
+
throw new InvalidAdapterError('Cannot copy from this adapter: it holds no _config record.');
|
|
42
|
+
}
|
|
43
|
+
const [versions, journal] = await Promise.all([
|
|
44
|
+
from.getVersions(config.id),
|
|
45
|
+
from.getJournal(config.id),
|
|
46
|
+
]);
|
|
47
|
+
const to = await create({ config, history: { versions, journal } });
|
|
48
|
+
try {
|
|
49
|
+
const importRecord = to.importRecord?.bind(to);
|
|
50
|
+
if (!importRecord)
|
|
51
|
+
refuseTarget('it has no importRecord(), so it cannot write history');
|
|
52
|
+
const listHeld = to.listBlobs?.bind(to);
|
|
53
|
+
if (!listHeld)
|
|
54
|
+
refuseTarget('it has no listBlobs(), so it could not be copied from in turn');
|
|
55
|
+
await assertNewStore(to, config.content.ownerEntityId, listHeld);
|
|
56
|
+
for (const type of await from.listTypes())
|
|
57
|
+
await to.saveType(type);
|
|
58
|
+
// Bytes before the Records that name them, as every upload writes them.
|
|
59
|
+
for (const { fileId } of await listBlobs())
|
|
60
|
+
await to.putBlob(await from.getBlob(fileId));
|
|
61
|
+
await copyRecords(from, importRecord);
|
|
62
|
+
return to;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// The failure that stopped the copy is the one to report.
|
|
66
|
+
await to.close?.().catch(() => { });
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function refuseTarget(why) {
|
|
71
|
+
throw new InvalidAdapterError(`Cannot copy into the store create() returned: ${why}.`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A target is a store as its adapter creates one: the source's owner,
|
|
75
|
+
* checked as open() checks it, and nothing held yet — no Record, no Type, no
|
|
76
|
+
* blob. Every end of a copy lists all it holds, so a target is itself a
|
|
77
|
+
* store a later copy can read in full.
|
|
78
|
+
*/
|
|
79
|
+
async function assertNewStore(to, ownerEntityId, listHeld) {
|
|
80
|
+
if (to.ownerEntityId !== ownerEntityId) {
|
|
81
|
+
throw new OwnerMismatchError(ownerEntityId, to.ownerEntityId, 'the store create() returned');
|
|
82
|
+
}
|
|
83
|
+
if ((await to.queryRecords(resolveQuery({ ...EVERYTHING, limit: 1 }))).records.length) {
|
|
84
|
+
refuseTarget('it already holds Records');
|
|
85
|
+
}
|
|
86
|
+
if ((await to.listTypes()).length)
|
|
87
|
+
refuseTarget('it already holds Types');
|
|
88
|
+
if ((await listHeld()).length)
|
|
89
|
+
refuseTarget('it already holds blobs');
|
|
90
|
+
}
|
|
91
|
+
async function copyRecords(from, importRecord) {
|
|
92
|
+
let cursor;
|
|
93
|
+
do {
|
|
94
|
+
const page = await from.queryRecords(resolveQuery({ ...EVERYTHING, cursor }));
|
|
95
|
+
for (const record of page.records) {
|
|
96
|
+
const [versions, journal] = await Promise.all([
|
|
97
|
+
from.getVersions(record.id),
|
|
98
|
+
from.getJournal(record.id),
|
|
99
|
+
]);
|
|
100
|
+
await importRecord(record, { versions, journal });
|
|
101
|
+
}
|
|
102
|
+
cursor = page.cursor ?? undefined;
|
|
103
|
+
} while (cursor);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy.js","sourceRoot":"","sources":["../../src/adapter/copy.ts"],"names":[],"mappings":"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"}
|