@haverstack/core 0.39.0 → 0.41.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 +79 -16
- package/dist/access.js +1 -1
- package/dist/access.js.map +1 -1
- package/dist/adapter-entry.d.ts +2 -1
- package/dist/adapter-entry.d.ts.map +1 -1
- package/dist/adapter-entry.js +2 -1
- package/dist/adapter-entry.js.map +1 -1
- package/dist/changes.js +3 -3
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +2 -3
- package/dist/combine.js.map +1 -1
- package/dist/errors.d.ts +16 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -2
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +9 -1
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +34 -4
- package/dist/grants.js.map +1 -1
- package/dist/identity-bindings.d.ts.map +1 -1
- package/dist/identity-bindings.js +3 -0
- package/dist/identity-bindings.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +144 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +223 -0
- package/dist/install.js.map +1 -0
- package/dist/query-validation.d.ts +34 -1
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +101 -1
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +15 -1
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +33 -3
- package/dist/record-changes.js.map +1 -1
- package/dist/record-id.js +1 -1
- package/dist/record-id.js.map +1 -1
- package/dist/schema.d.ts +6 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +33 -10
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack.d.ts +50 -23
- package/dist/scoped-stack.d.ts.map +1 -1
- package/dist/scoped-stack.js +202 -129
- package/dist/scoped-stack.js.map +1 -1
- package/dist/stack.d.ts +182 -45
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +481 -192
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts +28 -11
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +42 -37
- package/dist/testing.js.map +1 -1
- package/dist/type-handle.d.ts +157 -0
- package/dist/type-handle.d.ts.map +1 -0
- package/dist/type-handle.js +131 -0
- package/dist/type-handle.js.map +1 -0
- package/dist/types.d.ts +91 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +7 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +131 -7
- package/dist/validate.js.map +1 -1
- package/dist/wire-body.d.ts +19 -3
- package/dist/wire-body.d.ts.map +1 -1
- package/dist/wire-body.js +99 -2
- package/dist/wire-body.js.map +1 -1
- package/dist/wire-entry.d.ts +2 -2
- package/dist/wire-entry.d.ts.map +1 -1
- package/dist/wire-entry.js +4 -4
- package/dist/wire-entry.js.map +1 -1
- package/dist/wire-request.d.ts +18 -1
- package/dist/wire-request.d.ts.map +1 -1
- package/dist/wire-request.js +33 -1
- package/dist/wire-request.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
// Core class and client interface
|
|
21
21
|
export { Stack } from './stack.js';
|
|
22
22
|
export { ScopedStack } from './scoped-stack.js';
|
|
23
|
+
export { isPlanEmpty } from './install.js';
|
|
24
|
+
// Type handles
|
|
25
|
+
export { typeHandle } from './type-handle.js';
|
|
23
26
|
// The error taxonomy
|
|
24
27
|
export { StackError, StackValidationError, StackMigrationError, StackPermissionError, StackNotFoundError, StackConflictError, StackVersionConflictError, StackBadRequestError, StackSchemaDriftError, StackPayloadTooLargeError, StackTimeoutError, } from './errors.js';
|
|
25
28
|
// Local errors outside the taxonomy — no wire representation, and no
|
|
@@ -32,4 +35,5 @@ export { SYSTEM_TYPES, NATIVE_SORT_FIELDS } from './types.js';
|
|
|
32
35
|
export { generateId } from './id.js';
|
|
33
36
|
export { hashSchema, isCompatible } from './schema.js';
|
|
34
37
|
export { applyMergePatch } from './merge.js';
|
|
38
|
+
export { assertOneSurface } from './record-changes.js';
|
|
35
39
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kCAAkC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kCAAkC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAgBhD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,eAAe;AACf,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAc9C,qBAAqB;AACrB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,qEAAqE;AACrE,oEAAoE;AACpE,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAgFvF,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE9D,YAAY;AAEZ,4EAA4E;AAC5E,sEAAsE;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App installs
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* An app the owner does not run ships a manifest — the types it defines
|
|
5
|
+
* and the type-level grants it asks for — and the owner applies it with
|
|
6
|
+
* `Stack.planInstall()` then `Stack.installApp()`. What the stack keeps is
|
|
7
|
+
* not the manifest but the owner's approval of it: an `_install` Record,
|
|
8
|
+
* one per `appId`, linked by association to the `_app` cards it was
|
|
9
|
+
* installed for and the `_grant` Records it produced. Its version history
|
|
10
|
+
* is the upgrade log, and soft-deleting it is uninstalling.
|
|
11
|
+
*
|
|
12
|
+
* An install claims the families it defines, one install per family. That
|
|
13
|
+
* claim is what lets a contained app commit migrations within its own
|
|
14
|
+
* families without the owner running its code — see
|
|
15
|
+
* ScopedStack.commitMigration().
|
|
16
|
+
*
|
|
17
|
+
* This module holds the parts that read an install as data: the write-time
|
|
18
|
+
* shape rules, the family claim, and the plan diff. The verbs that write
|
|
19
|
+
* live on `Stack`. See docs/spec/apps.md.
|
|
20
|
+
*/
|
|
21
|
+
import type { AppId, AuthorityAssociation, BaseId, EntityId, GrantContent, InstallContent, InstallRequest, RecordId, RelationshipAssociation, StackRecord, TypeId } from './types.js';
|
|
22
|
+
import type { DefineTypeOptions } from './stack.js';
|
|
23
|
+
import type { ValidationError } from './validate.js';
|
|
24
|
+
/** What an app ships: the types it defines and the grants it asks for. */
|
|
25
|
+
export type AppManifest = {
|
|
26
|
+
appId: AppId;
|
|
27
|
+
name: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
types: DefineTypeOptions[];
|
|
30
|
+
requests: InstallRequest[];
|
|
31
|
+
};
|
|
32
|
+
/** A request on a family this install does not define, and who owns that family. */
|
|
33
|
+
export type ForeignRequest = InstallRequest & {
|
|
34
|
+
/**
|
|
35
|
+
* The app whose namespace the family is in, whether or not it is
|
|
36
|
+
* installed; `'commons'` or `'system'` for families no app owns; null
|
|
37
|
+
* for a family with no namespace.
|
|
38
|
+
*/
|
|
39
|
+
owner: AppId | 'commons' | 'system' | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A manifest type whose definition would write: one not yet defined, or
|
|
43
|
+
* defined with a different schema or name. Commons types included, since
|
|
44
|
+
* defining one claims nothing but still fixes its shape for every app.
|
|
45
|
+
*/
|
|
46
|
+
export type TypeChange = {
|
|
47
|
+
id: TypeId;
|
|
48
|
+
change: 'new' | 'schema' | 'name';
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* What applying a manifest would change, for the owner to approve.
|
|
52
|
+
* `installApp()` applies a plan only while it is still what planning the
|
|
53
|
+
* same manifest would produce. See docs/spec/apps.md § Plan, then apply.
|
|
54
|
+
*/
|
|
55
|
+
export type InstallPlan = {
|
|
56
|
+
manifest: AppManifest;
|
|
57
|
+
/** The key being installed. */
|
|
58
|
+
did: EntityId;
|
|
59
|
+
/** The install as it stood when planned — null for a first install. */
|
|
60
|
+
existing: (StackRecord & {
|
|
61
|
+
content: InstallContent;
|
|
62
|
+
}) | null;
|
|
63
|
+
/** Families this install would claim that it does not claim yet. */
|
|
64
|
+
newFamilies: BaseId[];
|
|
65
|
+
/** Type versions not yet in the install's `defines`. */
|
|
66
|
+
newVersions: TypeId[];
|
|
67
|
+
requestsAdded: InstallRequest[];
|
|
68
|
+
requestsRemoved: InstallRequest[];
|
|
69
|
+
/** Requests on families the manifest does not define — the ones an approval most needs to show. */
|
|
70
|
+
foreignRequests: ForeignRequest[];
|
|
71
|
+
/** Every manifest type `installApp()` would define or redefine. */
|
|
72
|
+
typeChanges: TypeChange[];
|
|
73
|
+
/**
|
|
74
|
+
* Whether `did` would gain access: not yet linked to this install, or
|
|
75
|
+
* linked through an `_app` card that is soft-deleted, which installApp()
|
|
76
|
+
* undeletes. See docs/spec/apps.md § Plan, then apply.
|
|
77
|
+
*/
|
|
78
|
+
newKey: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The keys already linked to the install. Each holds `requests`, so a
|
|
81
|
+
* plan that changes them changes every one — and a new key joins them
|
|
82
|
+
* as the same app. See docs/spec/apps.md § Plan, then apply.
|
|
83
|
+
*/
|
|
84
|
+
linkedKeys: EntityId[];
|
|
85
|
+
};
|
|
86
|
+
/** Relationship label from an install to an `_app` card it was installed for. */
|
|
87
|
+
export declare const INSTALL_APP_LABEL = "install.app";
|
|
88
|
+
/** Relationship label from an install to a `_grant` it produced. */
|
|
89
|
+
export declare const INSTALL_GRANT_LABEL = "install.grant";
|
|
90
|
+
export declare const installAppLink: (recordId: RecordId) => RelationshipAssociation;
|
|
91
|
+
export declare const installGrantLink: (recordId: RecordId) => RelationshipAssociation;
|
|
92
|
+
/** Record-level `read` on an install for one of its keys. */
|
|
93
|
+
export declare const installReader: (entityId: EntityId) => AuthorityAssociation;
|
|
94
|
+
/** Record ids an install links to under `label`. */
|
|
95
|
+
export declare function linkedIds(record: StackRecord, label: string): RecordId[];
|
|
96
|
+
export declare const isSystemFamily: (baseId: BaseId) => boolean;
|
|
97
|
+
/** The Schema Commons namespace: families no app owns. See docs/commons/README.md. */
|
|
98
|
+
export declare const COMMONS_NAMESPACE = "org.haverstack";
|
|
99
|
+
/** The part of a family before its `/` — `com.example.notes` for `com.example.notes/note`. */
|
|
100
|
+
export declare const namespaceOf: (baseId: BaseId) => string | null;
|
|
101
|
+
/**
|
|
102
|
+
* How a family stands toward the app `appId`: its own (the family's
|
|
103
|
+
* namespace is the `appId`), a commons or system family nobody owns, or
|
|
104
|
+
* another app's. Only an app's own families can be claimed, so which app
|
|
105
|
+
* owns a family never depends on which was installed first.
|
|
106
|
+
* See docs/spec/apps.md § Who owns a family.
|
|
107
|
+
*/
|
|
108
|
+
export declare function familyStanding(baseId: BaseId, appId: AppId): 'own' | 'commons' | 'system' | 'foreign';
|
|
109
|
+
/**
|
|
110
|
+
* The families an install claims, read as data: entries that are not
|
|
111
|
+
* well-formed TypeIds claim nothing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function claimedFamilies(content: unknown): Set<BaseId>;
|
|
114
|
+
/**
|
|
115
|
+
* An `_install`'s `defines` and `requests`, asked of its content on every
|
|
116
|
+
* write. A schema can say both are lists; only this can say what their
|
|
117
|
+
* entries must name. See docs/spec/apps.md § The `_install` record.
|
|
118
|
+
*/
|
|
119
|
+
export declare function validateInstall(typeId: TypeId, content: unknown): ValidationError[];
|
|
120
|
+
/** The manifest's types in its own namespace — the versions an install of it defines. */
|
|
121
|
+
export declare function ownTypeIds(manifest: AppManifest): TypeId[];
|
|
122
|
+
/**
|
|
123
|
+
* A deep-frozen copy of `manifest`, so the plan that holds it applies what
|
|
124
|
+
* was reviewed even if the caller's object changes afterwards.
|
|
125
|
+
*/
|
|
126
|
+
export declare function snapshotManifest(manifest: AppManifest): AppManifest;
|
|
127
|
+
/** Whether two requests ask for the same family and exactly the same actions. */
|
|
128
|
+
export declare function sameRequest(a: InstallRequest, b: InstallRequest): boolean;
|
|
129
|
+
/** Whether a stored grant is exactly `request`, made out to `did`. */
|
|
130
|
+
export declare function grantIsRequest(grant: GrantContent, request: InstallRequest, did: EntityId): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Whether applying `plan` would change nothing: the install is live, this
|
|
133
|
+
* key is linked to it, the manifest adds and removes nothing, and its
|
|
134
|
+
* `name` and `version` are the ones the install holds. A server
|
|
135
|
+
* answers such a request as already installed rather than queuing it.
|
|
136
|
+
* See docs/spec/wire-format.md § Installs.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isPlanEmpty(plan: InstallPlan): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* The parts of a plan that depend on the stack's state — what
|
|
141
|
+
* `installApp()` compares to refuse a stale one.
|
|
142
|
+
*/
|
|
143
|
+
export declare function planFingerprint(plan: InstallPlan): string;
|
|
144
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App installs
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* An app the owner does not run ships a manifest — the types it defines
|
|
5
|
+
* and the type-level grants it asks for — and the owner applies it with
|
|
6
|
+
* `Stack.planInstall()` then `Stack.installApp()`. What the stack keeps is
|
|
7
|
+
* not the manifest but the owner's approval of it: an `_install` Record,
|
|
8
|
+
* one per `appId`, linked by association to the `_app` cards it was
|
|
9
|
+
* installed for and the `_grant` Records it produced. Its version history
|
|
10
|
+
* is the upgrade log, and soft-deleting it is uninstalling.
|
|
11
|
+
*
|
|
12
|
+
* An install claims the families it defines, one install per family. That
|
|
13
|
+
* claim is what lets a contained app commit migrations within its own
|
|
14
|
+
* families without the owner running its code — see
|
|
15
|
+
* ScopedStack.commitMigration().
|
|
16
|
+
*
|
|
17
|
+
* This module holds the parts that read an install as data: the write-time
|
|
18
|
+
* shape rules, the family claim, and the plan diff. The verbs that write
|
|
19
|
+
* live on `Stack`. See docs/spec/apps.md.
|
|
20
|
+
*/
|
|
21
|
+
import { baseIdOf, familyIdProblem, parseTypeId } from './schema.js';
|
|
22
|
+
import { GRANT_ACTION_SET, UNGRANTABLE_SYSTEM_TYPES } from './grants.js';
|
|
23
|
+
import { SYSTEM_TYPES } from './types.js';
|
|
24
|
+
/** Relationship label from an install to an `_app` card it was installed for. */
|
|
25
|
+
export const INSTALL_APP_LABEL = 'install.app';
|
|
26
|
+
/** Relationship label from an install to a `_grant` it produced. */
|
|
27
|
+
export const INSTALL_GRANT_LABEL = 'install.grant';
|
|
28
|
+
export const installAppLink = (recordId) => ({
|
|
29
|
+
kind: 'relationship',
|
|
30
|
+
label: INSTALL_APP_LABEL,
|
|
31
|
+
target: { kind: 'record', recordId },
|
|
32
|
+
});
|
|
33
|
+
export const installGrantLink = (recordId) => ({
|
|
34
|
+
kind: 'relationship',
|
|
35
|
+
label: INSTALL_GRANT_LABEL,
|
|
36
|
+
target: { kind: 'record', recordId },
|
|
37
|
+
});
|
|
38
|
+
/** Record-level `read` on an install for one of its keys. */
|
|
39
|
+
export const installReader = (entityId) => ({
|
|
40
|
+
kind: 'permission',
|
|
41
|
+
label: 'read',
|
|
42
|
+
grantee: { kind: 'entity', entityId },
|
|
43
|
+
});
|
|
44
|
+
/** Record ids an install links to under `label`. */
|
|
45
|
+
export function linkedIds(record, label) {
|
|
46
|
+
const ids = [];
|
|
47
|
+
for (const a of record.associations ?? []) {
|
|
48
|
+
if (a.kind === 'relationship' && a.label === label && a.target.kind === 'record') {
|
|
49
|
+
ids.push(a.target.recordId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return ids;
|
|
53
|
+
}
|
|
54
|
+
const SYSTEM_FAMILIES = new Set(Object.values(SYSTEM_TYPES));
|
|
55
|
+
export const isSystemFamily = (baseId) => SYSTEM_FAMILIES.has(baseId);
|
|
56
|
+
/** The Schema Commons namespace: families no app owns. See docs/commons/README.md. */
|
|
57
|
+
export const COMMONS_NAMESPACE = 'org.haverstack';
|
|
58
|
+
/** The part of a family before its `/` — `com.example.notes` for `com.example.notes/note`. */
|
|
59
|
+
export const namespaceOf = (baseId) => {
|
|
60
|
+
const slash = baseId.indexOf('/');
|
|
61
|
+
return slash > 0 ? baseId.slice(0, slash) : null;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* How a family stands toward the app `appId`: its own (the family's
|
|
65
|
+
* namespace is the `appId`), a commons or system family nobody owns, or
|
|
66
|
+
* another app's. Only an app's own families can be claimed, so which app
|
|
67
|
+
* owns a family never depends on which was installed first.
|
|
68
|
+
* See docs/spec/apps.md § Who owns a family.
|
|
69
|
+
*/
|
|
70
|
+
export function familyStanding(baseId, appId) {
|
|
71
|
+
if (isSystemFamily(baseId))
|
|
72
|
+
return 'system';
|
|
73
|
+
const namespace = namespaceOf(baseId);
|
|
74
|
+
if (namespace === COMMONS_NAMESPACE)
|
|
75
|
+
return 'commons';
|
|
76
|
+
return namespace === appId ? 'own' : 'foreign';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The families an install claims, read as data: entries that are not
|
|
80
|
+
* well-formed TypeIds claim nothing.
|
|
81
|
+
*/
|
|
82
|
+
export function claimedFamilies(content) {
|
|
83
|
+
const defines = content?.defines;
|
|
84
|
+
const families = new Set();
|
|
85
|
+
if (!Array.isArray(defines))
|
|
86
|
+
return families;
|
|
87
|
+
for (const id of defines) {
|
|
88
|
+
if (typeof id !== 'string')
|
|
89
|
+
continue;
|
|
90
|
+
const parsed = parseTypeId(id);
|
|
91
|
+
if (parsed)
|
|
92
|
+
families.add(parsed.baseId);
|
|
93
|
+
}
|
|
94
|
+
return families;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* An `_install`'s `defines` and `requests`, asked of its content on every
|
|
98
|
+
* write. A schema can say both are lists; only this can say what their
|
|
99
|
+
* entries must name. See docs/spec/apps.md § The `_install` record.
|
|
100
|
+
*/
|
|
101
|
+
export function validateInstall(typeId, content) {
|
|
102
|
+
if (baseIdOf(typeId) !== SYSTEM_TYPES.INSTALL)
|
|
103
|
+
return [];
|
|
104
|
+
const c = content;
|
|
105
|
+
const errors = [];
|
|
106
|
+
if (Array.isArray(c?.defines)) {
|
|
107
|
+
c.defines.forEach((id, i) => {
|
|
108
|
+
const parsed = typeof id === 'string' ? parseTypeId(id) : null;
|
|
109
|
+
if (!parsed) {
|
|
110
|
+
errors.push({ path: `defines[${i}]`, message: 'Expected a versioned TypeId' });
|
|
111
|
+
}
|
|
112
|
+
else if (typeof c?.appId === 'string' && familyStanding(parsed.baseId, c.appId) !== 'own') {
|
|
113
|
+
errors.push({
|
|
114
|
+
path: `defines[${i}]`,
|
|
115
|
+
message: `"${parsed.baseId}" is outside the namespace "${c.appId}"; an install claims only its own families`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(c?.requests)) {
|
|
121
|
+
c.requests.forEach((r, i) => {
|
|
122
|
+
const req = r;
|
|
123
|
+
const problem = familyIdProblem(req?.baseId, `requests[${i}].baseId`);
|
|
124
|
+
if (problem) {
|
|
125
|
+
errors.push({ path: `requests[${i}].baseId`, message: problem });
|
|
126
|
+
}
|
|
127
|
+
else if (UNGRANTABLE_SYSTEM_TYPES.has(req.baseId)) {
|
|
128
|
+
errors.push({
|
|
129
|
+
path: `requests[${i}].baseId`,
|
|
130
|
+
message: `"${String(req.baseId)}" cannot be granted, so no install can request it`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(req?.actions)) {
|
|
134
|
+
req.actions.forEach((a, j) => {
|
|
135
|
+
if (!GRANT_ACTION_SET.has(a)) {
|
|
136
|
+
errors.push({
|
|
137
|
+
path: `requests[${i}].actions[${j}]`,
|
|
138
|
+
message: `Unknown grant action "${String(a)}"`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return errors;
|
|
146
|
+
}
|
|
147
|
+
/** The manifest's types in its own namespace — the versions an install of it defines. */
|
|
148
|
+
export function ownTypeIds(manifest) {
|
|
149
|
+
const ids = manifest.types
|
|
150
|
+
.map((t) => t.id)
|
|
151
|
+
.filter((id) => familyStanding(baseIdOf(id), manifest.appId) === 'own');
|
|
152
|
+
return [...new Set(ids)];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A deep-frozen copy of `manifest`, so the plan that holds it applies what
|
|
156
|
+
* was reviewed even if the caller's object changes afterwards.
|
|
157
|
+
*/
|
|
158
|
+
export function snapshotManifest(manifest) {
|
|
159
|
+
return deepFreeze(structuredClone(manifest));
|
|
160
|
+
}
|
|
161
|
+
function deepFreeze(value) {
|
|
162
|
+
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
163
|
+
Object.freeze(value);
|
|
164
|
+
for (const v of Object.values(value))
|
|
165
|
+
deepFreeze(v);
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
/** Whether two requests ask for the same family and exactly the same actions. */
|
|
170
|
+
export function sameRequest(a, b) {
|
|
171
|
+
if (a.baseId !== b.baseId)
|
|
172
|
+
return false;
|
|
173
|
+
const as = new Set(a.actions);
|
|
174
|
+
const bs = new Set(b.actions);
|
|
175
|
+
return as.size === bs.size && [...bs].every((x) => as.has(x));
|
|
176
|
+
}
|
|
177
|
+
/** Whether a stored grant is exactly `request`, made out to `did`. */
|
|
178
|
+
export function grantIsRequest(grant, request, did) {
|
|
179
|
+
if (grant.grantee?.kind !== 'entity' || grant.grantee.entityId !== did)
|
|
180
|
+
return false;
|
|
181
|
+
if (!Array.isArray(grant.actions))
|
|
182
|
+
return false;
|
|
183
|
+
return sameRequest({ baseId: grant.baseId, actions: grant.actions }, request);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether applying `plan` would change nothing: the install is live, this
|
|
187
|
+
* key is linked to it, the manifest adds and removes nothing, and its
|
|
188
|
+
* `name` and `version` are the ones the install holds. A server
|
|
189
|
+
* answers such a request as already installed rather than queuing it.
|
|
190
|
+
* See docs/spec/wire-format.md § Installs.
|
|
191
|
+
*/
|
|
192
|
+
export function isPlanEmpty(plan) {
|
|
193
|
+
return (plan.existing !== null &&
|
|
194
|
+
!plan.existing.deletedAt &&
|
|
195
|
+
!plan.newKey &&
|
|
196
|
+
plan.manifest.name === plan.existing.content.name &&
|
|
197
|
+
(plan.manifest.version ?? null) === (plan.existing.content.version ?? null) &&
|
|
198
|
+
plan.newFamilies.length === 0 &&
|
|
199
|
+
plan.newVersions.length === 0 &&
|
|
200
|
+
plan.typeChanges.length === 0 &&
|
|
201
|
+
plan.requestsAdded.length === 0 &&
|
|
202
|
+
plan.requestsRemoved.length === 0);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The parts of a plan that depend on the stack's state — what
|
|
206
|
+
* `installApp()` compares to refuse a stale one.
|
|
207
|
+
*/
|
|
208
|
+
export function planFingerprint(plan) {
|
|
209
|
+
return JSON.stringify([
|
|
210
|
+
plan.existing?.id ?? null,
|
|
211
|
+
plan.existing?.version ?? null,
|
|
212
|
+
plan.existing?.deletedAt ? true : false,
|
|
213
|
+
plan.newFamilies,
|
|
214
|
+
plan.newVersions,
|
|
215
|
+
plan.requestsAdded,
|
|
216
|
+
plan.requestsRemoved,
|
|
217
|
+
plan.foreignRequests,
|
|
218
|
+
plan.typeChanges,
|
|
219
|
+
plan.newKey,
|
|
220
|
+
plan.linkedKeys,
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* See docs/spec/data-model.md § Capability-gated filters.
|
|
18
18
|
*/
|
|
19
19
|
import type { ValidationError } from './validate.js';
|
|
20
|
-
import type { Association, AuthorityAssociation, DataAssociation, GrantGrantee, JournalQuery, QuerySort, RecordFilter, RelationshipTarget, StackCapabilities } from './types.js';
|
|
20
|
+
import type { Association, AssociationEdit, AuthorityAssociation, DataAssociation, GrantGrantee, JournalQuery, VersionsQuery, QuerySort, RecordFilter, RelationshipTarget, StackCapabilities } from './types.js';
|
|
21
21
|
/**
|
|
22
22
|
* Fail loud rather than silently widen: a filter the adapter can't honor
|
|
23
23
|
* would otherwise be dropped, returning an unfiltered superset presented
|
|
@@ -53,6 +53,16 @@ export declare function parseContentFilterKey(key: string): string[];
|
|
|
53
53
|
* it. See docs/spec/data-model.md § Sorting and pagination.
|
|
54
54
|
*/
|
|
55
55
|
export declare function assertValidSort(sort: QuerySort | undefined): void;
|
|
56
|
+
/**
|
|
57
|
+
* The sort every adapter receives: a named sort with no `direction` is
|
|
58
|
+
* ascending, resolved here so no adapter carries its own default. No sort
|
|
59
|
+
* stays absent — the adapter's unsorted order is `createdAt` newest first,
|
|
60
|
+
* and an explicit sort would be re-checked against the server's declared
|
|
61
|
+
* `sort.fields`. See docs/spec/data-model.md § Sorting and pagination.
|
|
62
|
+
*/
|
|
63
|
+
export declare function normalizeSort(sort: QuerySort | undefined): (QuerySort & {
|
|
64
|
+
direction: 'asc' | 'desc';
|
|
65
|
+
}) | undefined;
|
|
56
66
|
/**
|
|
57
67
|
* Fail loud rather than silently reorder: an adapter that can't honor the
|
|
58
68
|
* requested sort would otherwise answer in some other order, which a
|
|
@@ -118,6 +128,15 @@ export declare function assertAuthorityAssociations(permissions: readonly Associ
|
|
|
118
128
|
* See docs/spec/data-model.md § Associations.
|
|
119
129
|
*/
|
|
120
130
|
export declare function validateAssociations(associations: Association[] | undefined, path?: string): ValidationError[];
|
|
131
|
+
/**
|
|
132
|
+
* Hold an association edit list to the rules every verb shares: non-empty,
|
|
133
|
+
* every element a known `op` over a well-formed association, one surface,
|
|
134
|
+
* and each identity named once — a list that both removes and adds one
|
|
135
|
+
* identity is the same ambiguity as naming it twice. `repoint` is the
|
|
136
|
+
* journal's word, never a request. Asked of the whole list before anything
|
|
137
|
+
* is read or written. See docs/spec/data-model.md § Mutations.
|
|
138
|
+
*/
|
|
139
|
+
export declare function assertAssociationEdits(changes: unknown, surface: string, half: 'data' | 'authority'): asserts changes is AssociationEdit[];
|
|
121
140
|
/**
|
|
122
141
|
* Reject an association filter that names neither of its halves, or whose
|
|
123
142
|
* relationship target is malformed. `RelatedToFilter` and `AttachmentFilter`
|
|
@@ -126,6 +145,14 @@ export declare function validateAssociations(associations: Association[] | undef
|
|
|
126
145
|
* any association of that kind. See docs/spec/data-model.md § Filter.
|
|
127
146
|
*/
|
|
128
147
|
export declare function assertValidAssociationFilters(filter: RecordFilter | undefined): void;
|
|
148
|
+
/**
|
|
149
|
+
* Refuse a `baseId` carrying an `@version` suffix. A family filter resolves
|
|
150
|
+
* against registered families, so a TypeId there would match nothing and
|
|
151
|
+
* say nothing. See docs/spec/data-model.md § Filter.
|
|
152
|
+
*/
|
|
153
|
+
export declare function assertValidBaseIdFilter(filter: {
|
|
154
|
+
baseId?: string | string[];
|
|
155
|
+
} | undefined): void;
|
|
129
156
|
/**
|
|
130
157
|
* Hold a journal window to the shape both adapters can honor identically.
|
|
131
158
|
*
|
|
@@ -140,4 +167,10 @@ export declare function assertValidAssociationFilters(filter: RecordFilter | und
|
|
|
140
167
|
* reconstructing an association's full history.
|
|
141
168
|
*/
|
|
142
169
|
export declare function assertValidJournalQuery(query: JournalQuery | undefined): void;
|
|
170
|
+
/**
|
|
171
|
+
* Refuse a `getVersions()` window the adapters would read differently, as
|
|
172
|
+
* assertValidJournalQuery() does for the journal. No ceiling is imposed:
|
|
173
|
+
* omitting `limit` reads every version by contract.
|
|
174
|
+
*/
|
|
175
|
+
export declare function assertValidVersionsQuery(query: VersionsQuery | undefined): void;
|
|
143
176
|
//# sourceMappingURL=query-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-validation.d.ts","sourceRoot":"","sources":["../src/query-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"query-validation.d.ts","sourceRoot":"","sources":["../src/query-validation.ts"],"names":[],"mappings":"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"}
|
package/dist/query-validation.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* the query — one rule, one answer.
|
|
17
17
|
* See docs/spec/data-model.md § Capability-gated filters.
|
|
18
18
|
*/
|
|
19
|
-
import { StackBadRequestError } from './errors.js';
|
|
19
|
+
import { StackBadRequestError, StackValidationError } from './errors.js';
|
|
20
|
+
import { familyIdProblem } from './schema.js';
|
|
20
21
|
import { associationEqual, isAuthorityAssociation } from './record-changes.js';
|
|
21
22
|
import { CONTENT_SEGMENT_METACHARACTERS, SEGMENT_METACHARACTER_RE } from './validate.js';
|
|
22
23
|
import { NATIVE_SORT_FIELDS } from './types.js';
|
|
@@ -132,6 +133,18 @@ export function assertValidSort(sort) {
|
|
|
132
133
|
throw new StackBadRequestError(`Invalid sort direction "${sort.direction}": expected "asc" or "desc".`);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* The sort every adapter receives: a named sort with no `direction` is
|
|
138
|
+
* ascending, resolved here so no adapter carries its own default. No sort
|
|
139
|
+
* stays absent — the adapter's unsorted order is `createdAt` newest first,
|
|
140
|
+
* and an explicit sort would be re-checked against the server's declared
|
|
141
|
+
* `sort.fields`. See docs/spec/data-model.md § Sorting and pagination.
|
|
142
|
+
*/
|
|
143
|
+
export function normalizeSort(sort) {
|
|
144
|
+
if (!sort)
|
|
145
|
+
return undefined;
|
|
146
|
+
return { ...sort, direction: sort.direction ?? 'asc' };
|
|
147
|
+
}
|
|
135
148
|
/**
|
|
136
149
|
* Fail loud rather than silently reorder: an adapter that can't honor the
|
|
137
150
|
* requested sort would otherwise answer in some other order, which a
|
|
@@ -384,6 +397,61 @@ export function validateAssociations(associations, path = 'associations') {
|
|
|
384
397
|
: []),
|
|
385
398
|
];
|
|
386
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Hold an association edit list to the rules every verb shares: non-empty,
|
|
402
|
+
* every element a known `op` over a well-formed association, one surface,
|
|
403
|
+
* and each identity named once — a list that both removes and adds one
|
|
404
|
+
* identity is the same ambiguity as naming it twice. `repoint` is the
|
|
405
|
+
* journal's word, never a request. Asked of the whole list before anything
|
|
406
|
+
* is read or written. See docs/spec/data-model.md § Mutations.
|
|
407
|
+
*/
|
|
408
|
+
export function assertAssociationEdits(changes, surface, half) {
|
|
409
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
410
|
+
throw new StackValidationError([
|
|
411
|
+
{ path: 'changes', message: `${surface} names at least one change.` },
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
const errors = [];
|
|
415
|
+
changes.forEach((raw, i) => {
|
|
416
|
+
const path = `changes[${i}]`;
|
|
417
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
418
|
+
errors.push({ path, message: `${path} must be an object.` });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const edit = raw;
|
|
422
|
+
if (edit.op !== 'repoint')
|
|
423
|
+
assertKnownKeys(edit, ['op', 'association'], path);
|
|
424
|
+
if (edit.op === 'repoint') {
|
|
425
|
+
errors.push({
|
|
426
|
+
path: `${path}.op`,
|
|
427
|
+
message: 'op: "repoint" is recorded by the journal, not requested. ' +
|
|
428
|
+
"Send { op: 'add', association }: an add naming an attachment the record already holds re-points it in place.",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else if (edit.op !== 'add' && edit.op !== 'remove') {
|
|
432
|
+
errors.push({ path: `${path}.op`, message: 'op must be "add" or "remove".' });
|
|
433
|
+
}
|
|
434
|
+
errors.push(...validateAssociation(edit.association, `${path}.association`));
|
|
435
|
+
});
|
|
436
|
+
if (errors.length > 0)
|
|
437
|
+
throw new StackValidationError(errors);
|
|
438
|
+
const associations = changes.map((c) => c.association);
|
|
439
|
+
if (half === 'data')
|
|
440
|
+
assertDataAssociations(associations, surface);
|
|
441
|
+
else
|
|
442
|
+
assertAuthorityAssociations(associations, surface);
|
|
443
|
+
const duplicates = [];
|
|
444
|
+
associations.forEach((a, i) => {
|
|
445
|
+
if (associations.slice(0, i).some((b) => associationEqual(a, b))) {
|
|
446
|
+
duplicates.push({
|
|
447
|
+
path: `changes[${i}]`,
|
|
448
|
+
message: `Duplicate association identity: ${a.kind} "${a.label}"${granteeSuffix(a)} is named more than once.`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
if (duplicates.length > 0)
|
|
453
|
+
throw new StackValidationError(duplicates);
|
|
454
|
+
}
|
|
387
455
|
/**
|
|
388
456
|
* Reject an association filter that names neither of its halves, or whose
|
|
389
457
|
* relationship target is malformed. `RelatedToFilter` and `AttachmentFilter`
|
|
@@ -410,6 +478,21 @@ export function assertValidAssociationFilters(filter) {
|
|
|
410
478
|
if (errors.length > 0)
|
|
411
479
|
throw new StackBadRequestError(errors[0].message);
|
|
412
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Refuse a `baseId` carrying an `@version` suffix. A family filter resolves
|
|
483
|
+
* against registered families, so a TypeId there would match nothing and
|
|
484
|
+
* say nothing. See docs/spec/data-model.md § Filter.
|
|
485
|
+
*/
|
|
486
|
+
export function assertValidBaseIdFilter(filter) {
|
|
487
|
+
if (filter?.baseId === undefined)
|
|
488
|
+
return;
|
|
489
|
+
const errors = (Array.isArray(filter.baseId) ? filter.baseId : [filter.baseId])
|
|
490
|
+
.map((b) => familyIdProblem(b, 'filter.baseId'))
|
|
491
|
+
.filter((m) => m !== null)
|
|
492
|
+
.map((message) => ({ path: 'filter.baseId', message }));
|
|
493
|
+
if (errors.length > 0)
|
|
494
|
+
throw new StackValidationError(errors);
|
|
495
|
+
}
|
|
413
496
|
/**
|
|
414
497
|
* Hold a journal window to the shape both adapters can honor identically.
|
|
415
498
|
*
|
|
@@ -435,4 +518,21 @@ export function assertValidJournalQuery(query) {
|
|
|
435
518
|
}
|
|
436
519
|
}
|
|
437
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Refuse a `getVersions()` window the adapters would read differently, as
|
|
523
|
+
* assertValidJournalQuery() does for the journal. No ceiling is imposed:
|
|
524
|
+
* omitting `limit` reads every version by contract.
|
|
525
|
+
*/
|
|
526
|
+
export function assertValidVersionsQuery(query) {
|
|
527
|
+
if (!query)
|
|
528
|
+
return;
|
|
529
|
+
for (const key of ['beforeVersion', 'limit']) {
|
|
530
|
+
const value = query[key];
|
|
531
|
+
if (value === undefined)
|
|
532
|
+
continue;
|
|
533
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
534
|
+
throw new StackBadRequestError(`Invalid versions ${key} ${String(value)}: expected a positive integer.`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
438
538
|
//# sourceMappingURL=query-validation.js.map
|