@haverstack/core 0.40.0 → 0.42.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 +121 -59
- package/dist/access.d.ts +34 -36
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +65 -42
- package/dist/access.js.map +1 -1
- package/dist/{combine.d.ts → adapter/combine.d.ts} +8 -1
- package/dist/adapter/combine.d.ts.map +1 -0
- package/dist/{combine.js → adapter/combine.js} +7 -0
- package/dist/adapter/combine.js.map +1 -0
- package/dist/{adapter-entry.d.ts → adapter/index.d.ts} +6 -5
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/{adapter-entry.js → adapter/index.js} +5 -4
- package/dist/adapter/index.js.map +1 -0
- package/dist/{sort.d.ts → adapter/sort.d.ts} +20 -20
- package/dist/adapter/sort.d.ts.map +1 -0
- package/dist/{sort.js → adapter/sort.js} +37 -27
- package/dist/adapter/sort.js.map +1 -0
- package/dist/associations/identity.d.ts +93 -0
- package/dist/associations/identity.d.ts.map +1 -0
- package/dist/associations/identity.js +198 -0
- package/dist/associations/identity.js.map +1 -0
- package/dist/associations/validation.d.ts +90 -0
- package/dist/associations/validation.d.ts.map +1 -0
- package/dist/associations/validation.js +339 -0
- package/dist/associations/validation.js.map +1 -0
- package/dist/attachment-records.d.ts +48 -0
- package/dist/attachment-records.d.ts.map +1 -0
- package/dist/attachment-records.js +52 -0
- package/dist/attachment-records.js.map +1 -0
- package/dist/collection.d.ts +182 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +407 -0
- package/dist/collection.js.map +1 -0
- package/dist/content-match.d.ts +25 -0
- package/dist/content-match.d.ts.map +1 -0
- package/dist/content-match.js +51 -0
- package/dist/content-match.js.map +1 -0
- package/dist/content-translation.d.ts +42 -0
- package/dist/content-translation.d.ts.map +1 -0
- package/dist/content-translation.js +108 -0
- package/dist/content-translation.js.map +1 -0
- package/dist/did/did.d.ts.map +1 -0
- package/dist/{did.js → did/did.js} +3 -4
- package/dist/did/did.js.map +1 -0
- package/dist/{did-entry.d.ts → did/index.d.ts} +1 -1
- package/dist/did/index.d.ts.map +1 -0
- package/dist/{did-entry.js → did/index.js} +3 -3
- package/dist/did/index.js.map +1 -0
- package/dist/errors.d.ts +36 -63
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +35 -58
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +20 -43
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +71 -134
- package/dist/grants.js.map +1 -1
- package/dist/id.d.ts +13 -31
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +13 -31
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +14 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +141 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +219 -0
- package/dist/install.js.map +1 -0
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +5 -11
- package/dist/merge.js.map +1 -1
- package/dist/migration-steps.d.ts +102 -0
- package/dist/migration-steps.d.ts.map +1 -0
- package/dist/migration-steps.js +293 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/query-validation.d.ts +28 -94
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +156 -336
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +2 -117
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +10 -212
- package/dist/record-changes.js.map +1 -1
- package/dist/schema.d.ts +18 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +59 -30
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack/authority.d.ts +156 -0
- package/dist/scoped-stack/authority.d.ts.map +1 -0
- package/dist/scoped-stack/authority.js +384 -0
- package/dist/scoped-stack/authority.js.map +1 -0
- package/dist/scoped-stack/scoped-stack.d.ts +338 -0
- package/dist/scoped-stack/scoped-stack.d.ts.map +1 -0
- package/dist/scoped-stack/scoped-stack.js +776 -0
- package/dist/scoped-stack/scoped-stack.js.map +1 -0
- package/dist/scoped-stack/subscription.d.ts +55 -0
- package/dist/scoped-stack/subscription.d.ts.map +1 -0
- package/dist/scoped-stack/subscription.js +110 -0
- package/dist/scoped-stack/subscription.js.map +1 -0
- package/dist/stack/attachments.d.ts +24 -0
- package/dist/stack/attachments.d.ts.map +1 -0
- package/dist/stack/attachments.js +113 -0
- package/dist/stack/attachments.js.map +1 -0
- package/dist/{changes.d.ts → stack/changes.d.ts} +29 -65
- package/dist/stack/changes.d.ts.map +1 -0
- package/dist/{changes.js → stack/changes.js} +32 -66
- package/dist/stack/changes.js.map +1 -0
- package/dist/stack/client.d.ts +271 -0
- package/dist/stack/client.d.ts.map +1 -0
- package/dist/stack/client.js +9 -0
- package/dist/stack/client.js.map +1 -0
- package/dist/stack/identity-bindings.d.ts.map +1 -0
- package/dist/{identity-bindings.js → stack/identity-bindings.js} +8 -14
- package/dist/stack/identity-bindings.js.map +1 -0
- package/dist/stack/install-app.d.ts +22 -0
- package/dist/stack/install-app.d.ts.map +1 -0
- package/dist/stack/install-app.js +260 -0
- package/dist/stack/install-app.js.map +1 -0
- package/dist/stack/integrity.d.ts +94 -0
- package/dist/stack/integrity.d.ts.map +1 -0
- package/dist/stack/integrity.js +268 -0
- package/dist/stack/integrity.js.map +1 -0
- package/dist/{limits.d.ts → stack/limits.d.ts} +2 -5
- package/dist/stack/limits.d.ts.map +1 -0
- package/dist/{limits.js → stack/limits.js} +3 -6
- package/dist/stack/limits.js.map +1 -0
- package/dist/stack/migrations.d.ts +59 -0
- package/dist/stack/migrations.d.ts.map +1 -0
- package/dist/stack/migrations.js +141 -0
- package/dist/stack/migrations.js.map +1 -0
- package/dist/{stack-reads.d.ts → stack/reads.d.ts} +33 -6
- package/dist/stack/reads.d.ts.map +1 -0
- package/dist/{stack-reads.js → stack/reads.js} +49 -7
- package/dist/stack/reads.js.map +1 -0
- package/dist/stack/record-id.d.ts +35 -0
- package/dist/stack/record-id.d.ts.map +1 -0
- package/dist/stack/record-id.js +82 -0
- package/dist/stack/record-id.js.map +1 -0
- package/dist/stack/stack.d.ts +438 -0
- package/dist/stack/stack.d.ts.map +1 -0
- package/dist/stack/stack.js +1366 -0
- package/dist/stack/stack.js.map +1 -0
- package/dist/stack/system-types.d.ts +16 -0
- package/dist/stack/system-types.d.ts.map +1 -0
- package/dist/stack/system-types.js +111 -0
- package/dist/stack/system-types.js.map +1 -0
- package/dist/stack/type-grants.d.ts +20 -0
- package/dist/stack/type-grants.d.ts.map +1 -0
- package/dist/stack/type-grants.js +69 -0
- package/dist/stack/type-grants.js.map +1 -0
- package/dist/stack/write-expectation.d.ts +78 -0
- package/dist/stack/write-expectation.d.ts.map +1 -0
- package/dist/stack/write-expectation.js +71 -0
- package/dist/stack/write-expectation.js.map +1 -0
- package/dist/testing.d.ts +21 -26
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +50 -70
- package/dist/testing.js.map +1 -1
- package/dist/type-handle.d.ts +67 -78
- package/dist/type-handle.d.ts.map +1 -1
- package/dist/type-handle.js +44 -111
- package/dist/type-handle.js.map +1 -1
- package/dist/types/adapter.d.ts +223 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +5 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/associations.d.ts +156 -0
- package/dist/types/associations.d.ts.map +1 -0
- package/dist/types/associations.js +13 -0
- package/dist/types/associations.js.map +1 -0
- package/dist/types/capabilities.d.ts +89 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +5 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/events.d.ts +155 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +5 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/ids.d.ts +43 -0
- package/dist/types/ids.d.ts.map +1 -0
- package/dist/types/ids.js +5 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +19 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/journal.d.ts +47 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +5 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/query.d.ts +185 -0
- package/dist/types/query.d.ts.map +1 -0
- package/dist/types/query.js +10 -0
- package/dist/types/query.js.map +1 -0
- package/dist/types/records.d.ts +76 -0
- package/dist/types/records.d.ts.map +1 -0
- package/dist/types/records.js +5 -0
- package/dist/types/records.js.map +1 -0
- package/dist/types/schema.d.ts +74 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +5 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/system.d.ts +161 -0
- package/dist/types/system.d.ts.map +1 -0
- package/dist/types/system.js +32 -0
- package/dist/types/system.js.map +1 -0
- package/dist/types/tokens.d.ts +39 -0
- package/dist/types/tokens.d.ts.map +1 -0
- package/dist/types/tokens.js +5 -0
- package/dist/types/tokens.js.map +1 -0
- package/dist/validate.d.ts +29 -70
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +65 -97
- package/dist/validate.js.map +1 -1
- package/dist/{attachment-download.d.ts → wire/attachment-download.d.ts} +0 -57
- package/dist/wire/attachment-download.d.ts.map +1 -0
- package/dist/{attachment-download.js → wire/attachment-download.js} +0 -61
- package/dist/wire/attachment-download.js.map +1 -0
- package/dist/{auth.d.ts → wire/auth.d.ts} +5 -8
- package/dist/wire/auth.d.ts.map +1 -0
- package/dist/{auth.js → wire/auth.js} +5 -8
- package/dist/wire/auth.js.map +1 -0
- package/dist/{wire-body.d.ts → wire/body.d.ts} +13 -5
- package/dist/wire/body.d.ts.map +1 -0
- package/dist/{wire-body.js → wire/body.js} +65 -7
- package/dist/wire/body.js.map +1 -0
- package/dist/{wire-entry.d.ts → wire/index.d.ts} +12 -12
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/{wire-entry.js → wire/index.js} +13 -17
- package/dist/wire/index.js.map +1 -0
- package/dist/{wire-record.d.ts → wire/record.d.ts} +10 -22
- package/dist/wire/record.d.ts.map +1 -0
- package/dist/{wire-record.js → wire/record.js} +12 -24
- package/dist/wire/record.js.map +1 -0
- package/dist/{wire-request.d.ts → wire/request.d.ts} +20 -37
- package/dist/wire/request.d.ts.map +1 -0
- package/dist/{wire-request.js → wire/request.js} +79 -54
- package/dist/wire/request.js.map +1 -0
- package/package.json +7 -7
- package/dist/adapter-entry.d.ts.map +0 -1
- package/dist/adapter-entry.js.map +0 -1
- package/dist/attachment-download.d.ts.map +0 -1
- package/dist/attachment-download.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/changes.d.ts.map +0 -1
- package/dist/changes.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/did-entry.d.ts.map +0 -1
- package/dist/did-entry.js.map +0 -1
- package/dist/did.d.ts.map +0 -1
- package/dist/did.js.map +0 -1
- package/dist/identity-bindings.d.ts.map +0 -1
- package/dist/identity-bindings.js.map +0 -1
- package/dist/limits.d.ts.map +0 -1
- package/dist/limits.js.map +0 -1
- package/dist/record-id.d.ts +0 -70
- package/dist/record-id.d.ts.map +0 -1
- package/dist/record-id.js +0 -127
- package/dist/record-id.js.map +0 -1
- package/dist/scoped-stack.d.ts +0 -532
- package/dist/scoped-stack.d.ts.map +0 -1
- package/dist/scoped-stack.js +0 -1219
- package/dist/scoped-stack.js.map +0 -1
- package/dist/sort.d.ts.map +0 -1
- package/dist/sort.js.map +0 -1
- package/dist/stack-reads.d.ts.map +0 -1
- package/dist/stack-reads.js.map +0 -1
- package/dist/stack.d.ts +0 -971
- package/dist/stack.d.ts.map +0 -1
- package/dist/stack.js +0 -2012
- package/dist/stack.js.map +0 -1
- package/dist/types.d.ts +0 -1250
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -49
- package/dist/types.js.map +0 -1
- package/dist/wire-body.d.ts.map +0 -1
- package/dist/wire-body.js.map +0 -1
- package/dist/wire-entry.d.ts.map +0 -1
- package/dist/wire-entry.js.map +0 -1
- package/dist/wire-record.d.ts.map +0 -1
- package/dist/wire-record.js.map +0 -1
- package/dist/wire-request.d.ts.map +0 -1
- package/dist/wire-request.js.map +0 -1
- /package/dist/{did.d.ts → did/did.d.ts} +0 -0
- /package/dist/{identity-bindings.d.ts → stack/identity-bindings.d.ts} +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Association validation
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The rules an association is held to before it is stored: a known kind,
|
|
5
|
+
* only the keys that kind defines, a well-formed target or grantee, the
|
|
6
|
+
* right half of the authority/data partition, and each identity named
|
|
7
|
+
* once. A discriminated union is a compile-time promise, and a server
|
|
8
|
+
* mapping a request body onto one supplies raw JSON no compiler has seen,
|
|
9
|
+
* so the rules live in the invariant layer where no adapter can forget
|
|
10
|
+
* one. See docs/spec/data-model.md § Associations.
|
|
11
|
+
*/
|
|
12
|
+
import { ARGUMENTS_INVALID, StackBadRequestError, StackValidationError } from '../errors.js';
|
|
13
|
+
import { associationEqual, isAuthorityAssociation } from './identity.js';
|
|
14
|
+
/** The identifier spaces a relationship target may name. */
|
|
15
|
+
const TARGET_KINDS = new Set(['record', 'entity', 'external']);
|
|
16
|
+
/**
|
|
17
|
+
* The keys of one arm of a union, listed as an object so the compiler
|
|
18
|
+
* holds the list to the type: a field added to the arm fails to compile
|
|
19
|
+
* here until it is listed.
|
|
20
|
+
*/
|
|
21
|
+
const keysOf = (keys) => Object.keys(keys);
|
|
22
|
+
/** Every key each relationship target arm defines. */
|
|
23
|
+
export const TARGET_KEYS = {
|
|
24
|
+
record: keysOf({ kind: true, recordId: true, stackUrl: true }),
|
|
25
|
+
entity: keysOf({ kind: true, entityId: true }),
|
|
26
|
+
external: keysOf({ kind: true, ns: true, id: true }),
|
|
27
|
+
};
|
|
28
|
+
/** Every key each association kind defines. */
|
|
29
|
+
const ASSOCIATION_KEYS = {
|
|
30
|
+
tag: keysOf({ kind: true, label: true }),
|
|
31
|
+
attachment: keysOf({
|
|
32
|
+
kind: true,
|
|
33
|
+
label: true,
|
|
34
|
+
fileId: true,
|
|
35
|
+
attachmentRecordId: true,
|
|
36
|
+
}),
|
|
37
|
+
relationship: keysOf({ kind: true, label: true, target: true }),
|
|
38
|
+
permission: keysOf({ kind: true, label: true, grantee: true }),
|
|
39
|
+
anyone: keysOf({ kind: true, label: true }),
|
|
40
|
+
};
|
|
41
|
+
/** Every key each grantee arm defines, the default tier included. */
|
|
42
|
+
export const GRANTEE_KEYS = {
|
|
43
|
+
entity: keysOf({ kind: true, entityId: true }),
|
|
44
|
+
group: keysOf({ kind: true, groupId: true, role: true }),
|
|
45
|
+
authenticated: keysOf({ kind: true }),
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The keys of `value` its arm does not define. An element carrying one is
|
|
49
|
+
* refused rather than stored: an adapter keeps only the keys it has
|
|
50
|
+
* columns for, so the rest would answer 200 and then vanish.
|
|
51
|
+
* See docs/spec/data-model.md § Associations.
|
|
52
|
+
*/
|
|
53
|
+
export function unknownKeys(value, keys) {
|
|
54
|
+
return Object.keys(value).filter((key) => !keys.includes(key));
|
|
55
|
+
}
|
|
56
|
+
/** unknownKeys() as a refusal. 400, not 422: the key addresses nothing. */
|
|
57
|
+
export function assertKnownKeys(value, keys, path) {
|
|
58
|
+
const unknown = unknownKeys(value, keys);
|
|
59
|
+
if (unknown.length > 0)
|
|
60
|
+
throw new StackBadRequestError(`Unknown key${unknown.length > 1 ? 's' : ''} in ${path}: ${unknown.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Collect what makes a relationship target malformed. Absence is
|
|
64
|
+
* meaningful on `stackUrl` and an external `id` — this stack, and the
|
|
65
|
+
* whole namespace — so every part that names something must be non-empty:
|
|
66
|
+
* an empty string stores and matches as though it were absent.
|
|
67
|
+
* See docs/spec/data-model.md § Relationship targets.
|
|
68
|
+
*/
|
|
69
|
+
export function targetErrors(target, path, opts = {}) {
|
|
70
|
+
const fail = (message) => [{ path, message }];
|
|
71
|
+
if (!target || typeof target !== 'object')
|
|
72
|
+
return fail('A relationship target must be an object.');
|
|
73
|
+
if (!TARGET_KINDS.has(target.kind)) {
|
|
74
|
+
return fail(`Unknown relationship target kind "${target.kind}": expected "record", "entity" or "external".`);
|
|
75
|
+
}
|
|
76
|
+
assertKnownKeys(target, TARGET_KEYS[target.kind], path);
|
|
77
|
+
if (target.kind === 'record') {
|
|
78
|
+
if (!target.recordId)
|
|
79
|
+
return fail('A record target requires a non-empty recordId.');
|
|
80
|
+
if (target.stackUrl !== undefined && !target.stackUrl) {
|
|
81
|
+
return fail("A record target's stackUrl must be non-empty; omit it to name this stack.");
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (target.kind === 'entity') {
|
|
86
|
+
return target.entityId ? [] : fail('An entity target requires a non-empty entityId.');
|
|
87
|
+
}
|
|
88
|
+
if (!target.ns)
|
|
89
|
+
return fail('An external target requires a non-empty ns.');
|
|
90
|
+
if (target.id === undefined) {
|
|
91
|
+
return opts.externalIdOptional ? [] : fail('An external target requires an id.');
|
|
92
|
+
}
|
|
93
|
+
return target.id ? [] : fail("An external target's id must be non-empty when present.");
|
|
94
|
+
}
|
|
95
|
+
/** The kinds an association may name — the closed set every surface reads. */
|
|
96
|
+
const ASSOCIATION_KINDS = new Set(['tag', 'attachment', 'relationship', 'permission', 'anyone']);
|
|
97
|
+
/**
|
|
98
|
+
* Whether a value is an association at all. Asked ahead of the partition
|
|
99
|
+
* checks, so a malformed element is named as one rather than reported as
|
|
100
|
+
* the wrong surface for a kind it never had — a request body supplies raw
|
|
101
|
+
* JSON, and `null` or `{}` is neither half of the partition.
|
|
102
|
+
*/
|
|
103
|
+
function namesKind(association) {
|
|
104
|
+
return (!!association &&
|
|
105
|
+
typeof association === 'object' &&
|
|
106
|
+
ASSOCIATION_KINDS.has(association.kind));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reject an association outside the closed set the types promise: a request
|
|
110
|
+
* body is raw JSON, and an unknown target kind would otherwise be stored as
|
|
111
|
+
* a Record in this stack. A key its kind does not define is a 400 at every
|
|
112
|
+
* depth, as on a request body. See docs/spec/data-model.md § Associations.
|
|
113
|
+
*/
|
|
114
|
+
export function validateAssociation(association, path = 'association') {
|
|
115
|
+
if (!namesKind(association)) {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
path: `${path}.kind`,
|
|
119
|
+
message: `Unknown association kind "${String(association?.kind)}": expected ${[...ASSOCIATION_KINDS].map((k) => `"${k}"`).join(', ')}.`,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
assertKnownKeys(association, ASSOCIATION_KEYS[association.kind], path);
|
|
124
|
+
if (association.kind === 'permission') {
|
|
125
|
+
return permissionErrors(association, path);
|
|
126
|
+
}
|
|
127
|
+
if (association.kind === 'anyone') {
|
|
128
|
+
return association.label === 'read'
|
|
129
|
+
? []
|
|
130
|
+
: [{ path: `${path}.label`, message: 'An `anyone` association carries only `read`.' }];
|
|
131
|
+
}
|
|
132
|
+
if (association.kind !== 'relationship')
|
|
133
|
+
return [];
|
|
134
|
+
return targetErrors(association.target, `${path}.target`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Who a duplicated permission names, since kind and label alone don't
|
|
138
|
+
* distinguish two grants of the same bit to different grantees.
|
|
139
|
+
*/
|
|
140
|
+
function granteeSuffix(association) {
|
|
141
|
+
if (association.kind !== 'permission')
|
|
142
|
+
return '';
|
|
143
|
+
const g = association.grantee;
|
|
144
|
+
return g.kind === 'entity' ? ` for "${g.entityId}"` : ` for ${g.role}s of "${g.groupId}"`;
|
|
145
|
+
}
|
|
146
|
+
/** The bits a permission element may name — the whole of its meaning. */
|
|
147
|
+
const PERMISSION_LABELS = new Set(['read', 'write']);
|
|
148
|
+
/** A permission element's label and grantee — see granteeErrors(). */
|
|
149
|
+
function permissionErrors(association, path) {
|
|
150
|
+
if (!PERMISSION_LABELS.has(association.label)) {
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
path: `${path}.label`,
|
|
154
|
+
message: `Unknown permission label "${association.label}": expected "read" or "write".`,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
const grantee = association.grantee;
|
|
159
|
+
if (!grantee || typeof grantee !== 'object') {
|
|
160
|
+
return [{ path: `${path}.grantee`, message: 'A permission requires a grantee.' }];
|
|
161
|
+
}
|
|
162
|
+
return granteeErrors(grantee, `${path}.grantee`, { unknownKeys: 'throw' });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* What makes a grantee malformed, under `path`: first a key its tier does not
|
|
166
|
+
* define — a 400 under `throw`, as on a request body, or collected on a
|
|
167
|
+
* stored `_grant` — then its tier's own fields. `role` has no default to fall
|
|
168
|
+
* back on. `authenticated` is a `_grant` tier alone; `anyRole` admits `'any'`.
|
|
169
|
+
*/
|
|
170
|
+
export function granteeErrors(grantee, path, opts) {
|
|
171
|
+
const g = (grantee ?? {});
|
|
172
|
+
const tier = g.kind === 'entity' || g.kind === 'group' || (opts.authenticated && g.kind === 'authenticated')
|
|
173
|
+
? g.kind
|
|
174
|
+
: null;
|
|
175
|
+
if (tier === null) {
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
path: `${path}.kind`,
|
|
179
|
+
message: opts.authenticated
|
|
180
|
+
? "A grantee must name its tier: 'entity', 'group' or 'authenticated'."
|
|
181
|
+
: "A grantee must name its tier: 'entity' or 'group'.",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
if (opts.unknownKeys === 'throw')
|
|
186
|
+
assertKnownKeys(g, GRANTEE_KEYS[tier], path);
|
|
187
|
+
const unknown = unknownKeys(g, GRANTEE_KEYS[tier]);
|
|
188
|
+
if (unknown.length > 0) {
|
|
189
|
+
const article = tier === 'group' ? 'A' : 'An';
|
|
190
|
+
return unknown.map((key) => ({
|
|
191
|
+
path: `${path}.${key}`,
|
|
192
|
+
message: `${article} ${tier} grantee does not carry ${key}.`,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
if (tier === 'authenticated')
|
|
196
|
+
return [];
|
|
197
|
+
if (tier === 'entity') {
|
|
198
|
+
return typeof g.entityId === 'string' && g.entityId.length > 0
|
|
199
|
+
? []
|
|
200
|
+
: [{ path: `${path}.entityId`, message: 'An entity grantee requires a non-empty entityId.' }];
|
|
201
|
+
}
|
|
202
|
+
const errors = [];
|
|
203
|
+
if (typeof g.groupId !== 'string' || g.groupId.length === 0) {
|
|
204
|
+
errors.push({
|
|
205
|
+
path: `${path}.groupId`,
|
|
206
|
+
message: 'A group grantee requires a non-empty groupId.',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (g.role !== 'member' && g.role !== 'admin' && !(opts.anyRole && g.role === 'any')) {
|
|
210
|
+
errors.push({
|
|
211
|
+
path: `${path}.role`,
|
|
212
|
+
message: opts.anyRole
|
|
213
|
+
? "A group grantee requires role 'member', 'admin' or 'any'."
|
|
214
|
+
: "A group grantee requires role 'member' or 'admin'.",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return errors;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Refuse an authority element on the data surface. The two halves share
|
|
221
|
+
* storage but never a call, or an app editing tags could replace an ACL it
|
|
222
|
+
* was never shown. Thrown, not collected: the caller named the wrong
|
|
223
|
+
* surface, and wanted grantAccess()/revokeAccess().
|
|
224
|
+
* See docs/spec/access-control.md § Record-level permissions.
|
|
225
|
+
*/
|
|
226
|
+
export function assertDataAssociations(associations, surface) {
|
|
227
|
+
const authority = associations.find((a) => namesKind(a) && isAuthorityAssociation(a));
|
|
228
|
+
if (!authority)
|
|
229
|
+
return;
|
|
230
|
+
throw new StackBadRequestError(`${surface} does not carry authority: a "${authority.kind}" association belongs to the ` +
|
|
231
|
+
'`permissions` surface — use grantAccess()/revokeAccess(), or the `permissions` change-set key.');
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The mirror of assertDataAssociations(): the `permissions` surface takes
|
|
235
|
+
* authority kinds alone, so a tag routed through it is refused rather than
|
|
236
|
+
* quietly becoming an ACL entry the `associations` projection never shows.
|
|
237
|
+
*/
|
|
238
|
+
export function assertAuthorityAssociations(permissions, surface) {
|
|
239
|
+
const data = permissions.find((a) => namesKind(a) && !isAuthorityAssociation(a));
|
|
240
|
+
if (!data)
|
|
241
|
+
return;
|
|
242
|
+
throw new StackBadRequestError(`${surface} carries authority alone: a "${data.kind}" association belongs to the ` +
|
|
243
|
+
'`associations` surface — use associate()/dissociate(), or the `associations` change-set key.');
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Every association in a list, plus the list's own rule: identities are
|
|
247
|
+
* distinct. An adapter keys associations by identity, so a repeat would
|
|
248
|
+
* displace the first and leave which one stands to chance — a caller bug,
|
|
249
|
+
* refused rather than collapsed. See docs/spec/data-model.md § Associations.
|
|
250
|
+
*/
|
|
251
|
+
export function validateAssociations(associations, path = 'associations') {
|
|
252
|
+
const list = associations ?? [];
|
|
253
|
+
return [
|
|
254
|
+
...list.flatMap((a, i) => validateAssociation(a, `${path}[${i}]`)),
|
|
255
|
+
...duplicateIdentityErrors(list, path),
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
/** Each element naming an identity an earlier element already named. */
|
|
259
|
+
function duplicateIdentityErrors(list, path) {
|
|
260
|
+
return list.flatMap((a, i) => list.slice(0, i).some((b) => associationEqual(a, b))
|
|
261
|
+
? [
|
|
262
|
+
{
|
|
263
|
+
path: `${path}[${i}]`,
|
|
264
|
+
message: `Duplicate association identity: ${a.kind} "${a.label}"${granteeSuffix(a)} is named more than once.`,
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
: []);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Hold an edit list to the rules every verb shares: non-empty, known `op`s
|
|
271
|
+
* over well-formed associations, one surface, each identity once — removing
|
|
272
|
+
* and adding one identity is as ambiguous as naming it twice. `repoint` is
|
|
273
|
+
* the journal's word, never a request. See docs/spec/data-model.md § Mutations.
|
|
274
|
+
*/
|
|
275
|
+
export function assertAssociationEdits(changes, surface, half) {
|
|
276
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
277
|
+
throw new StackValidationError([{ path: 'changes', message: `${surface} names at least one change.` }], ARGUMENTS_INVALID);
|
|
278
|
+
}
|
|
279
|
+
const errors = [];
|
|
280
|
+
changes.forEach((raw, i) => {
|
|
281
|
+
const path = `changes[${i}]`;
|
|
282
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
283
|
+
errors.push({ path, message: `${path} must be an object.` });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const edit = raw;
|
|
287
|
+
if (edit.op !== 'repoint')
|
|
288
|
+
assertKnownKeys(edit, ['op', 'association'], path);
|
|
289
|
+
if (edit.op === 'repoint') {
|
|
290
|
+
errors.push({
|
|
291
|
+
path: `${path}.op`,
|
|
292
|
+
message: 'op: "repoint" is recorded by the journal, not requested. ' +
|
|
293
|
+
"Send { op: 'add', association }: an add naming an attachment the record already holds re-points it in place.",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else if (edit.op !== 'add' && edit.op !== 'remove') {
|
|
297
|
+
errors.push({ path: `${path}.op`, message: 'op must be "add" or "remove".' });
|
|
298
|
+
}
|
|
299
|
+
errors.push(...validateAssociation(edit.association, `${path}.association`));
|
|
300
|
+
});
|
|
301
|
+
if (errors.length > 0)
|
|
302
|
+
throw new StackValidationError(errors, ARGUMENTS_INVALID);
|
|
303
|
+
const associations = changes.map((c) => c.association);
|
|
304
|
+
if (half === 'data')
|
|
305
|
+
assertDataAssociations(associations, surface);
|
|
306
|
+
else
|
|
307
|
+
assertAuthorityAssociations(associations, surface);
|
|
308
|
+
const duplicates = duplicateIdentityErrors(associations, 'changes');
|
|
309
|
+
if (duplicates.length > 0)
|
|
310
|
+
throw new StackValidationError(duplicates, ARGUMENTS_INVALID);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* assertAssociationEdits() for the verbs taking a bare list — associate(),
|
|
314
|
+
* grantAccess() and their inverses — asked before wrapping, so problems are
|
|
315
|
+
* reported under `param`, the name the caller used. Same order of checks,
|
|
316
|
+
* so a wrong-surface element is refused as such before duplicates count.
|
|
317
|
+
*/
|
|
318
|
+
export function assertAssociationList(associations, surface, param, half) {
|
|
319
|
+
if (!Array.isArray(associations) || associations.length === 0) {
|
|
320
|
+
throw new StackValidationError([{ path: param, message: `${surface} names at least one association.` }], ARGUMENTS_INVALID);
|
|
321
|
+
}
|
|
322
|
+
const errors = associations.flatMap((raw, i) => typeof raw === 'object' && raw !== null && !Array.isArray(raw)
|
|
323
|
+
? []
|
|
324
|
+
: [{ path: `${param}[${i}]`, message: `${param}[${i}] must be an object.` }]);
|
|
325
|
+
if (errors.length > 0)
|
|
326
|
+
throw new StackValidationError(errors, ARGUMENTS_INVALID);
|
|
327
|
+
const list = associations;
|
|
328
|
+
const shapeErrors = list.flatMap((a, i) => validateAssociation(a, `${param}[${i}]`));
|
|
329
|
+
if (shapeErrors.length > 0)
|
|
330
|
+
throw new StackValidationError(shapeErrors, ARGUMENTS_INVALID);
|
|
331
|
+
if (half === 'data')
|
|
332
|
+
assertDataAssociations(list, surface);
|
|
333
|
+
else
|
|
334
|
+
assertAuthorityAssociations(list, surface);
|
|
335
|
+
const listErrors = validateAssociations(list, param);
|
|
336
|
+
if (listErrors.length > 0)
|
|
337
|
+
throw new StackValidationError(listErrors, ARGUMENTS_INVALID);
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/associations/validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment metadata records
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Which of a fileId's `_attachment` records answers for it: the one that
|
|
5
|
+
* establishes its mimeType, and the one a given reference names. Pure and
|
|
6
|
+
* generic over the record shape, so `Stack` and a server's own rows reach
|
|
7
|
+
* the same answer — `@haverstack/core/wire` exports the selections, and
|
|
8
|
+
* this module imports nothing so that entry point stays light.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Which `_attachment@1` record establishes a fileId's mimeType: earliest
|
|
12
|
+
* `createdAt`, ties to the lower `id`. Two racing first uploads can both
|
|
13
|
+
* land, so determinism rests on core's conflict check and every server
|
|
14
|
+
* ordering through here. The caller filters to one fileId.
|
|
15
|
+
* See docs/spec/attachments.md § Finding a `fileId`'s metadata records.
|
|
16
|
+
*/
|
|
17
|
+
export declare function firstRecordedAttachment<T extends {
|
|
18
|
+
id: string;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
}>(records: readonly T[]): T | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The same total order as a comparator, for a caller handing back the whole
|
|
23
|
+
* candidate set rather than the winner — sorted this way, the winner is
|
|
24
|
+
* element zero. Package-internal: `core/wire` exports the selection,
|
|
25
|
+
* since the ordering is only useful to something that already has every
|
|
26
|
+
* record.
|
|
27
|
+
*/
|
|
28
|
+
export declare function compareRecordedAttachments<T extends {
|
|
29
|
+
id: string;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
}>(a: T, b: T): number;
|
|
32
|
+
/**
|
|
33
|
+
* Which `_attachment` record a *reference* to a fileId names: its
|
|
34
|
+
* `attachmentRecordId`, else the requester's own upload, else the
|
|
35
|
+
* first-recorded — each step falling back rather than failing.
|
|
36
|
+
* See docs/spec/attachments.md § Naming the upload a reference came from.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveReferencedAttachment<T extends {
|
|
39
|
+
id: string;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
createdBy?: {
|
|
42
|
+
subjectId: string;
|
|
43
|
+
};
|
|
44
|
+
}>(records: readonly T[], opts?: {
|
|
45
|
+
attachmentRecordId?: string;
|
|
46
|
+
requesterEntityId?: string;
|
|
47
|
+
}): T | undefined;
|
|
48
|
+
//# sourceMappingURL=attachment-records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-records.d.ts","sourceRoot":"","sources":["../src/attachment-records.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,EAC/E,OAAO,EAAE,SAAS,CAAC,EAAE,GACpB,CAAC,GAAG,SAAS,CAKf;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,EAClF,CAAC,EAAE,CAAC,EACJ,CAAC,EAAE,CAAC,GACH,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EAE5E,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,IAAI,GAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GACrE,CAAC,GAAG,SAAS,CAUf"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment metadata records
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Which of a fileId's `_attachment` records answers for it: the one that
|
|
5
|
+
* establishes its mimeType, and the one a given reference names. Pure and
|
|
6
|
+
* generic over the record shape, so `Stack` and a server's own rows reach
|
|
7
|
+
* the same answer — `@haverstack/core/wire` exports the selections, and
|
|
8
|
+
* this module imports nothing so that entry point stays light.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Which `_attachment@1` record establishes a fileId's mimeType: earliest
|
|
12
|
+
* `createdAt`, ties to the lower `id`. Two racing first uploads can both
|
|
13
|
+
* land, so determinism rests on core's conflict check and every server
|
|
14
|
+
* ordering through here. The caller filters to one fileId.
|
|
15
|
+
* See docs/spec/attachments.md § Finding a `fileId`'s metadata records.
|
|
16
|
+
*/
|
|
17
|
+
export function firstRecordedAttachment(records) {
|
|
18
|
+
return records.reduce((first, record) => (!first || compareRecordedAttachments(record, first) < 0 ? record : first), undefined);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The same total order as a comparator, for a caller handing back the whole
|
|
22
|
+
* candidate set rather than the winner — sorted this way, the winner is
|
|
23
|
+
* element zero. Package-internal: `core/wire` exports the selection,
|
|
24
|
+
* since the ordering is only useful to something that already has every
|
|
25
|
+
* record.
|
|
26
|
+
*/
|
|
27
|
+
export function compareRecordedAttachments(a, b) {
|
|
28
|
+
const delta = a.createdAt.getTime() - b.createdAt.getTime();
|
|
29
|
+
if (delta !== 0)
|
|
30
|
+
return delta;
|
|
31
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Which `_attachment` record a *reference* to a fileId names: its
|
|
35
|
+
* `attachmentRecordId`, else the requester's own upload, else the
|
|
36
|
+
* first-recorded — each step falling back rather than failing.
|
|
37
|
+
* See docs/spec/attachments.md § Naming the upload a reference came from.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveReferencedAttachment(records, opts = {}) {
|
|
40
|
+
if (opts.attachmentRecordId) {
|
|
41
|
+
const named = records.find((record) => record.id === opts.attachmentRecordId);
|
|
42
|
+
if (named)
|
|
43
|
+
return named;
|
|
44
|
+
}
|
|
45
|
+
if (opts.requesterEntityId) {
|
|
46
|
+
const own = records.filter((record) => record.createdBy?.subjectId === opts.requesterEntityId);
|
|
47
|
+
if (own.length > 0)
|
|
48
|
+
return firstRecordedAttachment(own);
|
|
49
|
+
}
|
|
50
|
+
return firstRecordedAttachment(records);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=attachment-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-records.js","sourceRoot":"","sources":["../src/attachment-records.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAqB;IAErB,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,0BAA0B,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAC7F,SAAS,CACV,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,CAAI,EACJ,CAAI;IAEJ,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAC5D,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9B,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CAGzC,OAAqB,EACrB,OAAoE,EAAE;IAEtE,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC9E,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC/F,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections — typed access to one type family
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* `client.collection(handle)` returns a `Collection<C>`: every verb that
|
|
5
|
+
* addresses records of the handle's family, each taking and returning the
|
|
6
|
+
* handle's content type with one signature. Implemented once over the
|
|
7
|
+
* public `StackClient` verbs, so `Stack` and `ScopedStack` share it and a
|
|
8
|
+
* collection can do nothing its client could not.
|
|
9
|
+
*
|
|
10
|
+
* The collection's scope is the handle's whole family. Each record is read
|
|
11
|
+
* as stored and migrated in memory to the handle's version, one at a time,
|
|
12
|
+
* so a record the handle cannot represent — one stored at a newer version —
|
|
13
|
+
* is reported as a misfit rather than failing the page it sits on. A
|
|
14
|
+
* content write to a record at an older version commits its migration as a
|
|
15
|
+
* write of its own first. See docs/spec/data-model.md § Type handles.
|
|
16
|
+
*/
|
|
17
|
+
import type { MigrationRegistry } from './stack/migrations.js';
|
|
18
|
+
import type { CreateRecordOptions, DeleteRecordOptions, DeleteResult, StackClient } from './stack/client.js';
|
|
19
|
+
import type { ContentOf, ReadonlyTypeSchema, TypeHandle, UnlistedValue } from './type-handle.js';
|
|
20
|
+
import type { AssociationEdit, AuthorityAssociation, ChangeFilter, DataAssociation, IfVersionOptions, NativeSortField, RecordChange, RecordChangeSet, RecordFilter, RecordId, StackQuery, StackType, StackRecord, SubscribeOptions, Unsubscribe } from './types/index.js';
|
|
21
|
+
/**
|
|
22
|
+
* Content `C` as a read returns it: an enum may hold a value the handle
|
|
23
|
+
* does not list, since an enum can widen in place. Never flattens `C`, so
|
|
24
|
+
* an app that names its content with an `interface` keeps that name.
|
|
25
|
+
*/
|
|
26
|
+
export type StoredContent<C> = C extends string ? string extends C ? C : C | UnlistedValue : C extends readonly (infer I)[] ? StoredContent<I>[] : C extends object ? {
|
|
27
|
+
[K in keyof C]: StoredContent<C[K]>;
|
|
28
|
+
} : C;
|
|
29
|
+
/**
|
|
30
|
+
* The `contentPatch` content `C` accepts: every field optional, and `null`
|
|
31
|
+
* (removal) only on a field that is optional in `C`.
|
|
32
|
+
*/
|
|
33
|
+
export type ContentPatch<C> = {
|
|
34
|
+
[K in keyof C]?: Pick<C, K> extends Required<Pick<C, K>> ? C[K] : Exclude<C[K], undefined> | null;
|
|
35
|
+
};
|
|
36
|
+
/** A record of the collection, migrated to the handle's version. */
|
|
37
|
+
export interface CollectionRecord<C> extends Omit<StackRecord, 'content'> {
|
|
38
|
+
content: StoredContent<C>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A record in the handle's family that the handle cannot represent. Stored
|
|
42
|
+
* at a version newer than the handle's, it cannot be migrated down.
|
|
43
|
+
*/
|
|
44
|
+
export type Misfit = {
|
|
45
|
+
id: RecordId;
|
|
46
|
+
reason: 'newer-version';
|
|
47
|
+
/** Untyped, as stored. */
|
|
48
|
+
record: StackRecord;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A page of a collection. Misfits do not count as records, so a page can
|
|
52
|
+
* hold fewer than `limit` records with a cursor still set: the results end
|
|
53
|
+
* when `cursor` is `null`, not when a page comes back short.
|
|
54
|
+
*/
|
|
55
|
+
export type CollectionPage<C> = {
|
|
56
|
+
records: CollectionRecord<C>[];
|
|
57
|
+
misfits: Misfit[];
|
|
58
|
+
cursor: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Set when records at a version a content filter or sort could not reach
|
|
61
|
+
* match, or may match, the rest of the query. On the first page only,
|
|
62
|
+
* since it depends on the query rather than the page. See
|
|
63
|
+
* docs/spec/data-model.md § Content queries across versions.
|
|
64
|
+
*/
|
|
65
|
+
skipped?: {
|
|
66
|
+
older: boolean;
|
|
67
|
+
newer: boolean;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Path typing stops at this depth and accepts any suffix below it, since a
|
|
72
|
+
* recursive path type otherwise exceeds the compiler's instantiation limit.
|
|
73
|
+
* A typing limit, not a query limit: see docs/spec/data-model.md § Nested
|
|
74
|
+
* content paths.
|
|
75
|
+
*/
|
|
76
|
+
type PathDepth = [never, 0, 1, 2, 3, 4];
|
|
77
|
+
/** What a filter value can equal: an adapter compares the value a path reaches. */
|
|
78
|
+
type Leaf = string | number | boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Each content path of `V` reached at `P`, paired with the value a filter
|
|
81
|
+
* compares there. An array is matched element-wise, so it is its element.
|
|
82
|
+
* An object never equals a filter value, so its path takes only `null`.
|
|
83
|
+
*/
|
|
84
|
+
type PathEntry<P extends string, V, D extends number> = [D] extends [never] ? [P | `${P}.${string}`, Leaf] : V extends readonly (infer E)[] ? unknown extends E ? [P | `${P}.${string}`, Leaf] : PathEntry<P, E, PathDepth[D]> : V extends object ? string extends keyof V ? [P, null] | [`${P}.${string}`, Leaf] : [P, null] | FieldEntry<V, `${P}.`, PathDepth[D]> : [P, V];
|
|
85
|
+
/** `Required<C>[K]` rather than `NonNullable`, which would leak into errors. */
|
|
86
|
+
type FieldEntry<C, Prefix extends string, D extends number> = [D] extends [never] ? [`${Prefix}${string}`, Leaf] : {
|
|
87
|
+
[K in keyof C & string]-?: PathEntry<`${Prefix}${K}`, Required<C>[K], D>;
|
|
88
|
+
}[keyof C & string];
|
|
89
|
+
/**
|
|
90
|
+
* Every content path of `C`: its fields, and dotted paths through declared
|
|
91
|
+
* objects and arrays. Below an `open` object or array, or six segments
|
|
92
|
+
* deep, any suffix is a path.
|
|
93
|
+
*/
|
|
94
|
+
export type ContentPathOf<C> = FieldEntry<C, '', 5>[0];
|
|
95
|
+
/** The top-level scalar fields of `C`, the ones a content sort reaches. */
|
|
96
|
+
export type SortableFieldOf<C> = string extends keyof C ? string : {
|
|
97
|
+
[K in keyof C & string]-?: Required<C>[K] extends string | number | boolean ? K : never;
|
|
98
|
+
}[keyof C & string];
|
|
99
|
+
/**
|
|
100
|
+
* A collection reads live records of its own family only, as stored, so
|
|
101
|
+
* none of `typeId`, `baseId`, `includeDeleted`, `presentAt` or the per-Type
|
|
102
|
+
* shapes it generates itself is offered. Content filter keys and the sort
|
|
103
|
+
* field are checked against `C`, and carried to older versions through the
|
|
104
|
+
* migrations. See docs/spec/data-model.md § Type handles.
|
|
105
|
+
*/
|
|
106
|
+
export type CollectionQuery<C> = Omit<StackQuery, 'filter' | 'presentAt' | 'sort'> & {
|
|
107
|
+
filter?: Omit<RecordFilter, 'typeId' | 'baseId' | 'includeDeleted' | 'content' | 'contentPresent' | 'contentByType'> & {
|
|
108
|
+
content?: {
|
|
109
|
+
[E in FieldEntry<C, '', 5> as E[0]]?: E[1] | null;
|
|
110
|
+
};
|
|
111
|
+
contentPresent?: ContentPathOf<C>[];
|
|
112
|
+
};
|
|
113
|
+
sort?: {
|
|
114
|
+
field: NativeSortField;
|
|
115
|
+
contentField?: never;
|
|
116
|
+
direction?: 'asc' | 'desc';
|
|
117
|
+
} | {
|
|
118
|
+
field?: never;
|
|
119
|
+
contentField: SortableFieldOf<C>;
|
|
120
|
+
direction?: 'asc' | 'desc';
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** The family is the collection's, so neither `typeId` nor `baseId` is a filter key. */
|
|
124
|
+
export type CollectionSubscribeOptions = Omit<SubscribeOptions, 'filter'> & {
|
|
125
|
+
filter?: Omit<ChangeFilter, 'typeId' | 'baseId'>;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* A change to a record of the family. `record`, when present, is migrated
|
|
129
|
+
* to the handle's version; `typeId` stays the stored one. A change whose
|
|
130
|
+
* record does not fit carries `misfit` instead, and one whose record is a
|
|
131
|
+
* tombstone carries neither.
|
|
132
|
+
*/
|
|
133
|
+
export type CollectionChange<C> = Omit<RecordChange, 'record'> & {
|
|
134
|
+
record?: CollectionRecord<C>;
|
|
135
|
+
misfit?: Misfit;
|
|
136
|
+
};
|
|
137
|
+
export type CollectionChangeSet<C> = Omit<RecordChangeSet, 'contentPatch'> & {
|
|
138
|
+
contentPatch?: ContentPatch<C>;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Typed access to one type family. History (`getVersions`, `getVersion`,
|
|
142
|
+
* `getJournal`) spans versions by nature and stays on `StackClient`. See
|
|
143
|
+
* docs/spec/data-model.md § Type handles.
|
|
144
|
+
*/
|
|
145
|
+
export interface Collection<C> {
|
|
146
|
+
/** `null` for a missing record, a tombstone, or a record of another family. */
|
|
147
|
+
get(id: RecordId): Promise<CollectionRecord<C> | null>;
|
|
148
|
+
query(query?: CollectionQuery<C>): Promise<CollectionPage<C>>;
|
|
149
|
+
subscribe(handler: (change: CollectionChange<C>) => void, opts?: CollectionSubscribeOptions): Promise<Unsubscribe>;
|
|
150
|
+
create(content: C, opts?: CreateRecordOptions): Promise<CollectionRecord<C>>;
|
|
151
|
+
mutate(id: RecordId, changes: CollectionChangeSet<C>, opts?: IfVersionOptions): Promise<CollectionRecord<C>>;
|
|
152
|
+
patchContent(id: RecordId, patch: ContentPatch<C>, opts?: IfVersionOptions): Promise<CollectionRecord<C>>;
|
|
153
|
+
associate(id: RecordId, associations: DataAssociation[]): Promise<CollectionRecord<C>>;
|
|
154
|
+
dissociate(id: RecordId, associations: DataAssociation[]): Promise<CollectionRecord<C>>;
|
|
155
|
+
amendAssociations(id: RecordId, changes: AssociationEdit[]): Promise<CollectionRecord<C>>;
|
|
156
|
+
grantAccess(id: RecordId, permissions: AuthorityAssociation[]): Promise<CollectionRecord<C>>;
|
|
157
|
+
revokeAccess(id: RecordId, permissions: AuthorityAssociation[]): Promise<CollectionRecord<C>>;
|
|
158
|
+
amendAccess(id: RecordId, changes: AssociationEdit[]): Promise<CollectionRecord<C>>;
|
|
159
|
+
delete(id: RecordId, opts?: DeleteRecordOptions): Promise<DeleteResult>;
|
|
160
|
+
undelete(id: RecordId, opts?: IfVersionOptions): Promise<CollectionRecord<C>>;
|
|
161
|
+
restoreVersion(id: RecordId, version: number, opts?: IfVersionOptions): Promise<CollectionRecord<C>>;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A record the collection asked for does not fit the handle. Raised only
|
|
165
|
+
* in the app's own process and never on the wire, so it sits outside the
|
|
166
|
+
* `StackError` taxonomy. A write that raises it after writing says so.
|
|
167
|
+
* See docs/spec/wire-format.md § The taxonomy root.
|
|
168
|
+
*/
|
|
169
|
+
export declare class MisfitError extends Error {
|
|
170
|
+
readonly reason: Misfit['reason'];
|
|
171
|
+
readonly record: StackRecord;
|
|
172
|
+
constructor(misfit: Misfit, handle: TypeHandle, written?: boolean);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The collection over `handle`, shared by `Stack.collection()` and
|
|
176
|
+
* `ScopedStack.collection()`. `registry` is the opening `Stack`'s, which
|
|
177
|
+
* the app's own process filled. `listTypes` names the family's versions,
|
|
178
|
+
* which `StackClient` does not offer.
|
|
179
|
+
*/
|
|
180
|
+
export declare const createCollection: <S extends ReadonlyTypeSchema>(client: StackClient, registry: MigrationRegistry, handle: TypeHandle<S>, listTypes: () => Promise<StackType[]>) => Collection<ContentOf<S>>;
|
|
181
|
+
export {};
|
|
182
|
+
//# sourceMappingURL=collection.d.ts.map
|