@haverstack/core 0.29.0 → 0.31.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/dist/adapter-entry.d.ts +2 -2
- package/dist/adapter-entry.d.ts.map +1 -1
- package/dist/adapter-entry.js +1 -1
- package/dist/adapter-entry.js.map +1 -1
- package/dist/changes.d.ts +14 -0
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +35 -0
- package/dist/changes.js.map +1 -1
- package/dist/errors.d.ts +191 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +219 -0
- package/dist/errors.js.map +1 -0
- package/dist/grants.d.ts +84 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +157 -0
- package/dist/grants.js.map +1 -0
- package/dist/id.d.ts +7 -5
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +17 -15
- package/dist/id.js.map +1 -1
- package/dist/identity-bindings.d.ts +13 -0
- package/dist/identity-bindings.d.ts.map +1 -0
- package/dist/identity-bindings.js +42 -0
- package/dist/identity-bindings.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +23 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +35 -0
- package/dist/limits.js.map +1 -0
- package/dist/query-validation.d.ts +83 -0
- package/dist/query-validation.d.ts.map +1 -0
- package/dist/query-validation.js +228 -0
- package/dist/query-validation.js.map +1 -0
- package/dist/record-changes.d.ts +89 -0
- package/dist/record-changes.d.ts.map +1 -0
- package/dist/record-changes.js +201 -0
- package/dist/record-changes.js.map +1 -0
- package/dist/record-id.d.ts +70 -0
- package/dist/record-id.d.ts.map +1 -0
- package/dist/record-id.js +127 -0
- package/dist/record-id.js.map +1 -0
- package/dist/scoped-stack.d.ts +468 -0
- package/dist/scoped-stack.d.ts.map +1 -0
- package/dist/scoped-stack.js +1173 -0
- package/dist/scoped-stack.js.map +1 -0
- package/dist/stack-reads.d.ts +38 -0
- package/dist/stack-reads.d.ts.map +1 -0
- package/dist/stack-reads.js +79 -0
- package/dist/stack-reads.js.map +1 -0
- package/dist/stack.d.ts +37 -643
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +102 -2254
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -1
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/wire-entry.d.ts +3 -2
- package/dist/wire-entry.d.ts.map +1 -1
- package/dist/wire-entry.js +6 -2
- package/dist/wire-entry.js.map +1 -1
- package/dist/wire-record.js +1 -1
- package/dist/wire-record.js.map +1 -1
- package/dist/wire-request.d.ts +14 -0
- package/dist/wire-request.d.ts.map +1 -1
- package/dist/wire-request.js +19 -2
- package/dist/wire-request.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query and association validation
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The sanitizers a query passes through before any adapter sees it, plus
|
|
5
|
+
* the relationship-target rules an association is held to. Both sides of
|
|
6
|
+
* the same job: a discriminated union and a `QuerySort` field are compile-
|
|
7
|
+
* time promises, and a server mapping a request body onto either supplies
|
|
8
|
+
* raw JSON that no compiler has seen.
|
|
9
|
+
*
|
|
10
|
+
* These live in the invariant layer for the reason validation and
|
|
11
|
+
* `_config` protection do — so no adapter can forget one. `assertQueryCapabilities`
|
|
12
|
+
* and its neighbours are shared verbatim with `adapter-api`, which asks the
|
|
13
|
+
* same questions of a request arriving over the wire. A refusal names the
|
|
14
|
+
* capability it was refused for on the error itself, so a client reporting
|
|
15
|
+
* it in its own vocabulary reads that name rather than re-deriving it from
|
|
16
|
+
* the query — one rule, one answer.
|
|
17
|
+
* See docs/spec/data-model.md § Capability-gated filters.
|
|
18
|
+
*/
|
|
19
|
+
import { StackQueryError } from './errors.js';
|
|
20
|
+
import { CONTENT_SEGMENT_METACHARACTERS, SEGMENT_METACHARACTER_RE } from './validate.js';
|
|
21
|
+
import { NATIVE_SORT_FIELDS } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Fail loud rather than silently widen: a filter the adapter can't honor
|
|
24
|
+
* would otherwise be dropped, returning an unfiltered superset presented
|
|
25
|
+
* as the filtered result. Shared by Stack.query() and
|
|
26
|
+
* APIAdapter.queryRecords(). See docs/spec/data-model.md
|
|
27
|
+
* § Capability-gated filters.
|
|
28
|
+
*/
|
|
29
|
+
export function assertQueryCapabilities(filter, capabilities) {
|
|
30
|
+
const { content: reach, contentPresent, search } = capabilities.filter;
|
|
31
|
+
if (filter?.search && !search) {
|
|
32
|
+
throw new StackQueryError('Query uses filter.search, but this adapter does not declare the filter.search capability.', 'filter.search');
|
|
33
|
+
}
|
|
34
|
+
const present = filter?.contentPresent?.length ? filter.contentPresent : undefined;
|
|
35
|
+
if (!filter?.content && !present)
|
|
36
|
+
return;
|
|
37
|
+
if (reach === 'none') {
|
|
38
|
+
// Named as `filter.content` whichever key the query used: the reach
|
|
39
|
+
// is what is absent, and it is the entry to look at in discovery.
|
|
40
|
+
throw new StackQueryError(`Query uses ${present && !filter?.content ? 'filter.contentPresent' : 'filter.content'}, ` +
|
|
41
|
+
'but this adapter declares filter.content: "none".', 'filter.content');
|
|
42
|
+
}
|
|
43
|
+
if (present && !contentPresent) {
|
|
44
|
+
throw new StackQueryError('Query uses filter.contentPresent, but this adapter does not declare the ' +
|
|
45
|
+
'filter.contentPresent capability.', 'filter.contentPresent');
|
|
46
|
+
}
|
|
47
|
+
for (const key of [...Object.keys(filter?.content ?? {}), ...(present ?? [])]) {
|
|
48
|
+
if (parseContentFilterKey(key).length > 1 && reach !== 'path') {
|
|
49
|
+
throw new StackQueryError(`Query uses the nested content path "${key}", but this adapter declares ` +
|
|
50
|
+
`filter.content: "${reach}".`, 'filter.content');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a single-segment content filter can be pushed down to the
|
|
56
|
+
* adapter. Callers that read a content field pair this with an in-memory
|
|
57
|
+
* predicate over the wider result, so the query stays correct against an
|
|
58
|
+
* adapter that reaches no content at all — the rung comparison lives here
|
|
59
|
+
* rather than at each of them.
|
|
60
|
+
*/
|
|
61
|
+
export function filtersContent(features) {
|
|
62
|
+
return features.filter.content !== 'none';
|
|
63
|
+
}
|
|
64
|
+
/** The longest path both SQLite engines can execute — see the spec link below. */
|
|
65
|
+
const MAX_CONTENT_PATH_SEGMENTS = 32;
|
|
66
|
+
/**
|
|
67
|
+
* Split a content filter key into path segments. Write-time validation
|
|
68
|
+
* keeps stored field names free of the path metacharacters, but a filter
|
|
69
|
+
* arrives from a request body and has made no such promise, so the same
|
|
70
|
+
* rule is enforced here. See docs/spec/data-model.md § Content field names.
|
|
71
|
+
*/
|
|
72
|
+
export function parseContentFilterKey(key) {
|
|
73
|
+
const segments = key.split('.');
|
|
74
|
+
// A SQLite adapter spends two json_each joins per segment against a
|
|
75
|
+
// 64-table join limit, so a path past the cap is one the engine could
|
|
76
|
+
// not execute. See docs/spec/data-model.md § Nested content paths.
|
|
77
|
+
if (segments.length > MAX_CONTENT_PATH_SEGMENTS) {
|
|
78
|
+
throw new StackQueryError(`Invalid content filter path "${key}": at most ${MAX_CONTENT_PATH_SEGMENTS} segments.`);
|
|
79
|
+
}
|
|
80
|
+
for (const segment of segments) {
|
|
81
|
+
if (segment === '') {
|
|
82
|
+
throw new StackQueryError(`Invalid content filter path "${key}": a path segment cannot be empty.`);
|
|
83
|
+
}
|
|
84
|
+
if (SEGMENT_METACHARACTER_RE.test(segment)) {
|
|
85
|
+
throw new StackQueryError(`Invalid content filter path "${key}": a segment cannot contain any of ` +
|
|
86
|
+
`${CONTENT_SEGMENT_METACHARACTERS.join(' ')}.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return segments;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The only sort fields any adapter maps; anything else is a caller error.
|
|
93
|
+
* Narrowed against the one array NativeSortField is derived from, so this
|
|
94
|
+
* gate — which every query passes through before an adapter sees it —
|
|
95
|
+
* can't be the copy that a fourth native column is missing from.
|
|
96
|
+
*/
|
|
97
|
+
const VALID_SORT_FIELDS = new Set(NATIVE_SORT_FIELDS);
|
|
98
|
+
/** The only two sort directions; see assertValidSort. */
|
|
99
|
+
const VALID_SORT_DIRECTIONS = new Set(['asc', 'desc']);
|
|
100
|
+
/**
|
|
101
|
+
* Reject a sort whose field or direction is outside the closed set the
|
|
102
|
+
* types promise. `QuerySort` is typed `'asc' | 'desc'`, but a type is not a
|
|
103
|
+
* runtime guard: a server mapping `?direction=` onto a query, or a
|
|
104
|
+
* delegated app calling query(), supplies a raw string. A SQLite record
|
|
105
|
+
* adapter interpolates the direction straight into `ORDER BY`, so an
|
|
106
|
+
* unvalidated value there is a SQL-injection sink reachable from every
|
|
107
|
+
* untrusted caller. Validating in the invariant layer — the same reason
|
|
108
|
+
* emission and _config protection live here — means no adapter can forget
|
|
109
|
+
* it. See docs/spec/data-model.md § Sorting and pagination.
|
|
110
|
+
*/
|
|
111
|
+
export function assertValidSort(sort) {
|
|
112
|
+
if (!sort)
|
|
113
|
+
return;
|
|
114
|
+
if (sort.field !== undefined && sort.contentField !== undefined) {
|
|
115
|
+
throw new StackQueryError('A sort names either a native field or a content field, never both.');
|
|
116
|
+
}
|
|
117
|
+
if (sort.field !== undefined && !VALID_SORT_FIELDS.has(sort.field)) {
|
|
118
|
+
throw new StackQueryError(`Invalid sort field "${sort.field}": expected one of ${NATIVE_SORT_FIELDS.join(', ')}.`);
|
|
119
|
+
}
|
|
120
|
+
if (sort.contentField !== undefined) {
|
|
121
|
+
// Parsed by the same rule a filter key is, so a name a filter could
|
|
122
|
+
// never address is not one a sort can either — then held to one
|
|
123
|
+
// segment, because a value inside an array or object has no single
|
|
124
|
+
// position to order its record by (docs/spec/data-model.md
|
|
125
|
+
// § Sorting by a content field).
|
|
126
|
+
if (parseContentFilterKey(sort.contentField).length > 1) {
|
|
127
|
+
throw new StackQueryError(`Invalid sort content field "${sort.contentField}": sorting reaches top-level fields only.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (sort.direction !== undefined && !VALID_SORT_DIRECTIONS.has(sort.direction)) {
|
|
131
|
+
throw new StackQueryError(`Invalid sort direction "${sort.direction}": expected "asc" or "desc".`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fail loud rather than silently reorder: an adapter that can't honor the
|
|
136
|
+
* requested sort would otherwise answer in some other order, which a
|
|
137
|
+
* caller paging a bounded window has no way to notice. The companion to
|
|
138
|
+
* assertQueryCapabilities(), split from it because a sort is not a filter
|
|
139
|
+
* — see docs/spec/data-model.md § Capability-gated filters.
|
|
140
|
+
*/
|
|
141
|
+
export function assertSortCapability(sort, capabilities) {
|
|
142
|
+
if (!sort)
|
|
143
|
+
return;
|
|
144
|
+
if (sort.contentField !== undefined) {
|
|
145
|
+
if (!capabilities.sort.contentField) {
|
|
146
|
+
throw new StackQueryError('Query uses sort.contentField, but this adapter does not declare the sort.contentField ' +
|
|
147
|
+
'capability.', 'sort.contentField');
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const field = sort.field ?? 'createdAt';
|
|
152
|
+
if (!capabilities.sort.fields.includes(field)) {
|
|
153
|
+
throw new StackQueryError(`Query sorts by "${field}", which this adapter does not declare in sort.fields.`, 'sort.fields');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** The identifier spaces a relationship target may name. */
|
|
157
|
+
const TARGET_SCOPES = new Set(['record', 'entity', 'external']);
|
|
158
|
+
/**
|
|
159
|
+
* Collect what makes a relationship target malformed. Absence is
|
|
160
|
+
* meaningful on `stackUrl` and an external `id` — this stack, and the
|
|
161
|
+
* whole namespace — so every part that names something must be non-empty:
|
|
162
|
+
* an empty string stores and matches as though it were absent.
|
|
163
|
+
* See docs/spec/data-model.md § Relationship targets.
|
|
164
|
+
*/
|
|
165
|
+
function targetErrors(target, path, opts = {}) {
|
|
166
|
+
const fail = (message) => [{ path, message }];
|
|
167
|
+
if (!target || typeof target !== 'object')
|
|
168
|
+
return fail('A relationship target must be an object.');
|
|
169
|
+
if (!TARGET_SCOPES.has(target.scope)) {
|
|
170
|
+
return fail(`Unknown relationship target scope "${target.scope}": expected "record", "entity" or "external".`);
|
|
171
|
+
}
|
|
172
|
+
if (target.scope === 'record') {
|
|
173
|
+
if (!target.recordId)
|
|
174
|
+
return fail('A record target requires a non-empty recordId.');
|
|
175
|
+
if (target.stackUrl !== undefined && !target.stackUrl) {
|
|
176
|
+
return fail("A record target's stackUrl must be non-empty; omit it to name this stack.");
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
if (target.scope === 'entity') {
|
|
181
|
+
return target.entityId ? [] : fail('An entity target requires a non-empty entityId.');
|
|
182
|
+
}
|
|
183
|
+
if (!target.ns)
|
|
184
|
+
return fail('An external target requires a non-empty ns.');
|
|
185
|
+
if (target.id === undefined) {
|
|
186
|
+
return opts.externalIdOptional ? [] : fail('An external target requires an id.');
|
|
187
|
+
}
|
|
188
|
+
return target.id ? [] : fail("An external target's id must be non-empty when present.");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Reject a relationship target outside the closed set the types promise.
|
|
192
|
+
* A discriminated union is not a runtime guard — a server mapping a
|
|
193
|
+
* request body onto an association supplies raw JSON — and an
|
|
194
|
+
* unrecognized scope would otherwise be stored under the one arm that
|
|
195
|
+
* names a Record in this stack. See docs/spec/data-model.md
|
|
196
|
+
* § Relationship targets.
|
|
197
|
+
*/
|
|
198
|
+
export function validateAssociation(association, path = 'association') {
|
|
199
|
+
if (association?.kind !== 'relationship')
|
|
200
|
+
return [];
|
|
201
|
+
return targetErrors(association.target, `${path}.target`);
|
|
202
|
+
}
|
|
203
|
+
/** validateAssociation() over a create's `associations` array. */
|
|
204
|
+
export function validateAssociations(associations, path = 'associations') {
|
|
205
|
+
return (associations ?? []).flatMap((a, i) => validateAssociation(a, `${path}[${i}]`));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Reject a relationship filter that names neither a label nor a target,
|
|
209
|
+
* or whose target is malformed. `RelatedToFilter` promises one half is
|
|
210
|
+
* always present; without the runtime check a filter decoded from a
|
|
211
|
+
* request could arrive empty and match every record carrying any
|
|
212
|
+
* relationship. See docs/spec/data-model.md § Filter.
|
|
213
|
+
*/
|
|
214
|
+
export function assertValidRelatedTo(relatedTo) {
|
|
215
|
+
if (!relatedTo)
|
|
216
|
+
return;
|
|
217
|
+
if (relatedTo.label === undefined && relatedTo.target === undefined) {
|
|
218
|
+
throw new StackQueryError('filter.relatedTo must name a label, a target, or both — "any relationship at all" is not a filter.');
|
|
219
|
+
}
|
|
220
|
+
if (relatedTo.target === undefined)
|
|
221
|
+
return;
|
|
222
|
+
const errors = targetErrors(relatedTo.target, 'filter.relatedTo.target', {
|
|
223
|
+
externalIdOptional: true,
|
|
224
|
+
});
|
|
225
|
+
if (errors.length > 0)
|
|
226
|
+
throw new StackQueryError(errors[0].message);
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=query-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-validation.js","sourceRoot":"","sources":["../src/query-validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a write moved, and how a Record is presented
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Two jobs that share one question — what does this Record actually say?
|
|
5
|
+
*
|
|
6
|
+
* The equality helpers answer it against a proposed write, so the no-op
|
|
7
|
+
* decision and a change event's `ops` are the same comparison and can never
|
|
8
|
+
* disagree. The projections answer it against a reader: a soft-deleted
|
|
9
|
+
* Record is its tombstone, and a snapshot's `permissions` are owner-only
|
|
10
|
+
* audit data.
|
|
11
|
+
*
|
|
12
|
+
* See docs/spec/events.md § The event shape and docs/spec/versioning.md
|
|
13
|
+
* § The tombstone is literal.
|
|
14
|
+
*/
|
|
15
|
+
import type { Association, ChangeOp, EntityId, Permission, RecordChanges, RecordVersion, RelationshipTarget, StackRecord } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Matches the SQLite adapter's association primary key (kind, label,
|
|
18
|
+
* file_id, related_scope, related_id, related_ns, related_stack).
|
|
19
|
+
*/
|
|
20
|
+
export declare function associationEqual(a: Association, b: Association): boolean;
|
|
21
|
+
/** Structural equality per target arm — what dissociate() matches on. */
|
|
22
|
+
export declare function targetEqual(a: RelationshipTarget, b: RelationshipTarget): boolean;
|
|
23
|
+
export declare function permissionEqual(a: Permission, b: Permission): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A change set has to name at least one aspect. Refused rather than read
|
|
26
|
+
* as a no-op: it addresses nothing, so there is nothing it could have
|
|
27
|
+
* failed to satisfy, and every way of producing one is a caller bug —
|
|
28
|
+
* typically a conditional that built an empty object.
|
|
29
|
+
* See docs/spec/data-model.md § Mutations.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertNonEmptyChangeSet(changes: RecordChanges): void;
|
|
32
|
+
/**
|
|
33
|
+
* Which aspects a change set actually moves, against the record as it
|
|
34
|
+
* stands — the no-op decision and the change event's `ops` are the same
|
|
35
|
+
* comparison, so they can never disagree. A key naming the value a record
|
|
36
|
+
* already holds contributes nothing.
|
|
37
|
+
*
|
|
38
|
+
* `merged` is the content the patch produces, computed once by the
|
|
39
|
+
* caller that had to validate it anyway.
|
|
40
|
+
* See docs/spec/events.md § The event shape.
|
|
41
|
+
*/
|
|
42
|
+
export declare function changeSetOps(existing: StackRecord, changes: RecordChanges, merged: Record<string, unknown> | undefined): ChangeOp[];
|
|
43
|
+
/**
|
|
44
|
+
* The change set narrowed to the aspects that actually moved. An adapter
|
|
45
|
+
* is handed this rather than what the caller wrote, so restating an aspect
|
|
46
|
+
* cannot rewrite it: `unlisted: true` on an already-unlisted record would
|
|
47
|
+
* otherwise drag `unlistedAt` forward, moving the record's publish moment
|
|
48
|
+
* with no op reporting it. It also keeps the adapter contract honest —
|
|
49
|
+
* every key an adapter receives is one it must write.
|
|
50
|
+
*/
|
|
51
|
+
export declare function effectiveChanges(changes: RecordChanges, ops: ChangeOp[]): RecordChanges;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a merge patch produced the content the record already held.
|
|
54
|
+
* Compared by serialization: content is JSON by construction — it round
|
|
55
|
+
* trips through storage that way — and a patch preserves key order for
|
|
56
|
+
* every field it does not name, so the encoding of an unchanged record is
|
|
57
|
+
* stable. A reordering patch that changes nothing else is the one case
|
|
58
|
+
* this reports as a change, which costs an empty version rather than a
|
|
59
|
+
* wrong answer.
|
|
60
|
+
*/
|
|
61
|
+
export declare function contentEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean;
|
|
62
|
+
export declare function permissionsEqual(a: Permission[], b: Permission[]): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Ensures `creator` carries an `admin` relationship association, adding one
|
|
65
|
+
* if it's not already present. Used to bootstrap a `_group` record's first
|
|
66
|
+
* admin at create time.
|
|
67
|
+
*/
|
|
68
|
+
export declare function stampGroupAdmin(associations: Association[] | undefined, creator: EntityId): Association[];
|
|
69
|
+
/**
|
|
70
|
+
* Whether a Record is a `_group`, in any of its type versions — the family
|
|
71
|
+
* whose roster rules apply.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isGroupRecord(record: StackRecord): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Drops a snapshot's `permissions` — owner-only audit data, never served
|
|
76
|
+
* to a non-owner history reader. `entityId` (change attribution) stays.
|
|
77
|
+
* See docs/spec/versioning.md § History access.
|
|
78
|
+
*/
|
|
79
|
+
export declare function stripVersionPermissions(version: RecordVersion): RecordVersion;
|
|
80
|
+
/**
|
|
81
|
+
* The tombstone a soft-deleted Record is presented as. `permissions` is
|
|
82
|
+
* retained because it decides whether the caller may undelete; history is
|
|
83
|
+
* exempt, which is why getVersions() still serves the content this
|
|
84
|
+
* withholds. See docs/spec/versioning.md § The tombstone is literal.
|
|
85
|
+
*/
|
|
86
|
+
export declare function tombstoneOf(record: StackRecord): StackRecord;
|
|
87
|
+
/** A soft-deleted Record presented as its tombstone; anything else untouched. */
|
|
88
|
+
export declare const presentDeleted: (record: StackRecord) => StackRecord;
|
|
89
|
+
//# sourceMappingURL=record-changes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-changes.d.ts","sourceRoot":"","sources":["../src/record-changes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAOxE;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAQjF;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAUrE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAQpE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1C,QAAQ,EAAE,CAyBZ;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,aAAa,CAUvF;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAE5F;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,OAAO,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,WAAW,EAAE,GAAG,SAAS,EACvC,OAAO,EAAE,QAAQ,GAChB,WAAW,EAAE,CAcf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAI7E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAY5D;AAED,iFAAiF;AACjF,eAAO,MAAM,cAAc,GAAI,QAAQ,WAAW,KAAG,WACJ,CAAC"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a write moved, and how a Record is presented
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Two jobs that share one question — what does this Record actually say?
|
|
5
|
+
*
|
|
6
|
+
* The equality helpers answer it against a proposed write, so the no-op
|
|
7
|
+
* decision and a change event's `ops` are the same comparison and can never
|
|
8
|
+
* disagree. The projections answer it against a reader: a soft-deleted
|
|
9
|
+
* Record is its tombstone, and a snapshot's `permissions` are owner-only
|
|
10
|
+
* audit data.
|
|
11
|
+
*
|
|
12
|
+
* See docs/spec/events.md § The event shape and docs/spec/versioning.md
|
|
13
|
+
* § The tombstone is literal.
|
|
14
|
+
*/
|
|
15
|
+
import { baseIdOf } from './schema.js';
|
|
16
|
+
import { StackQueryError } from './errors.js';
|
|
17
|
+
import { SYSTEM_TYPES, RECORD_CHANGE_KEYS } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Matches the SQLite adapter's association primary key (kind, label,
|
|
20
|
+
* file_id, related_scope, related_id, related_ns, related_stack).
|
|
21
|
+
*/
|
|
22
|
+
export function associationEqual(a, b) {
|
|
23
|
+
if (a.kind !== b.kind || a.label !== b.label)
|
|
24
|
+
return false;
|
|
25
|
+
if (a.kind === 'attachment' && b.kind === 'attachment')
|
|
26
|
+
return a.fileId === b.fileId;
|
|
27
|
+
if (a.kind === 'relationship' && b.kind === 'relationship') {
|
|
28
|
+
return targetEqual(a.target, b.target);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Structural equality per target arm — what dissociate() matches on. */
|
|
33
|
+
export function targetEqual(a, b) {
|
|
34
|
+
if (a.scope !== b.scope)
|
|
35
|
+
return false;
|
|
36
|
+
if (a.scope === 'record' && b.scope === 'record') {
|
|
37
|
+
return a.recordId === b.recordId && (a.stackUrl ?? '') === (b.stackUrl ?? '');
|
|
38
|
+
}
|
|
39
|
+
if (a.scope === 'entity' && b.scope === 'entity')
|
|
40
|
+
return a.entityId === b.entityId;
|
|
41
|
+
if (a.scope === 'external' && b.scope === 'external')
|
|
42
|
+
return a.ns === b.ns && a.id === b.id;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
export function permissionEqual(a, b) {
|
|
46
|
+
if (a.access !== b.access)
|
|
47
|
+
return false;
|
|
48
|
+
if (a.access === 'public')
|
|
49
|
+
return true;
|
|
50
|
+
if (a.access === 'entity' && b.access === 'entity') {
|
|
51
|
+
return a.entityId === b.entityId && a.read === b.read && a.write === b.write;
|
|
52
|
+
}
|
|
53
|
+
if (a.access === 'group' && b.access === 'group') {
|
|
54
|
+
return a.groupId === b.groupId && a.role === b.role && a.read === b.read && a.write === b.write;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A change set has to name at least one aspect. Refused rather than read
|
|
60
|
+
* as a no-op: it addresses nothing, so there is nothing it could have
|
|
61
|
+
* failed to satisfy, and every way of producing one is a caller bug —
|
|
62
|
+
* typically a conditional that built an empty object.
|
|
63
|
+
* See docs/spec/data-model.md § Mutations.
|
|
64
|
+
*/
|
|
65
|
+
export function assertNonEmptyChangeSet(changes) {
|
|
66
|
+
// Presence, not truthiness: `unlisted: false` and `parentId: null` are
|
|
67
|
+
// aspects this call names, and reading them as absent would drop a
|
|
68
|
+
// change the caller asked for.
|
|
69
|
+
if (RECORD_CHANGE_KEYS.some((key) => changes[key] !== undefined))
|
|
70
|
+
return;
|
|
71
|
+
throw new StackQueryError('A change set names at least one of: ' + RECORD_CHANGE_KEYS.join(', ') + '.');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Which aspects a change set actually moves, against the record as it
|
|
75
|
+
* stands — the no-op decision and the change event's `ops` are the same
|
|
76
|
+
* comparison, so they can never disagree. A key naming the value a record
|
|
77
|
+
* already holds contributes nothing.
|
|
78
|
+
*
|
|
79
|
+
* `merged` is the content the patch produces, computed once by the
|
|
80
|
+
* caller that had to validate it anyway.
|
|
81
|
+
* See docs/spec/events.md § The event shape.
|
|
82
|
+
*/
|
|
83
|
+
export function changeSetOps(existing, changes, merged) {
|
|
84
|
+
const ops = [];
|
|
85
|
+
if (merged !== undefined && !contentEqual(existing.content, merged))
|
|
86
|
+
ops.push('patch');
|
|
87
|
+
if (changes.parentId !== undefined && changes.parentId !== (existing.parentId ?? null)) {
|
|
88
|
+
ops.push('reparent');
|
|
89
|
+
}
|
|
90
|
+
if (changes.permissions && !permissionsEqual(existing.permissions ?? [], changes.permissions)) {
|
|
91
|
+
ops.push('permissions');
|
|
92
|
+
}
|
|
93
|
+
if (changes.associations) {
|
|
94
|
+
const before = existing.associations ?? [];
|
|
95
|
+
const after = changes.associations;
|
|
96
|
+
if (after.some((a) => !before.some((b) => associationEqual(a, b))))
|
|
97
|
+
ops.push('associate');
|
|
98
|
+
if (before.some((b) => !after.some((a) => associationEqual(a, b))))
|
|
99
|
+
ops.push('dissociate');
|
|
100
|
+
}
|
|
101
|
+
if (changes.unlisted !== undefined && Boolean(existing.unlistedAt) !== changes.unlisted) {
|
|
102
|
+
ops.push(changes.unlisted ? 'unlist' : 'list');
|
|
103
|
+
}
|
|
104
|
+
return ops;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The change set narrowed to the aspects that actually moved. An adapter
|
|
108
|
+
* is handed this rather than what the caller wrote, so restating an aspect
|
|
109
|
+
* cannot rewrite it: `unlisted: true` on an already-unlisted record would
|
|
110
|
+
* otherwise drag `unlistedAt` forward, moving the record's publish moment
|
|
111
|
+
* with no op reporting it. It also keeps the adapter contract honest —
|
|
112
|
+
* every key an adapter receives is one it must write.
|
|
113
|
+
*/
|
|
114
|
+
export function effectiveChanges(changes, ops) {
|
|
115
|
+
const effective = {};
|
|
116
|
+
if (ops.includes('patch'))
|
|
117
|
+
effective.contentPatch = changes.contentPatch;
|
|
118
|
+
if (ops.includes('reparent'))
|
|
119
|
+
effective.parentId = changes.parentId;
|
|
120
|
+
if (ops.includes('permissions'))
|
|
121
|
+
effective.permissions = changes.permissions;
|
|
122
|
+
if (ops.includes('associate') || ops.includes('dissociate')) {
|
|
123
|
+
effective.associations = changes.associations;
|
|
124
|
+
}
|
|
125
|
+
if (ops.includes('unlist') || ops.includes('list'))
|
|
126
|
+
effective.unlisted = changes.unlisted;
|
|
127
|
+
return effective;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether a merge patch produced the content the record already held.
|
|
131
|
+
* Compared by serialization: content is JSON by construction — it round
|
|
132
|
+
* trips through storage that way — and a patch preserves key order for
|
|
133
|
+
* every field it does not name, so the encoding of an unchanged record is
|
|
134
|
+
* stable. A reordering patch that changes nothing else is the one case
|
|
135
|
+
* this reports as a change, which costs an empty version rather than a
|
|
136
|
+
* wrong answer.
|
|
137
|
+
*/
|
|
138
|
+
export function contentEqual(a, b) {
|
|
139
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
140
|
+
}
|
|
141
|
+
export function permissionsEqual(a, b) {
|
|
142
|
+
return a.length === b.length && a.every((p, i) => permissionEqual(p, b[i]));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Ensures `creator` carries an `admin` relationship association, adding one
|
|
146
|
+
* if it's not already present. Used to bootstrap a `_group` record's first
|
|
147
|
+
* admin at create time.
|
|
148
|
+
*/
|
|
149
|
+
export function stampGroupAdmin(associations, creator) {
|
|
150
|
+
const list = associations ?? [];
|
|
151
|
+
const alreadyAdmin = list.some((a) => a.kind === 'relationship' &&
|
|
152
|
+
a.label === 'admin' &&
|
|
153
|
+
a.target.scope === 'entity' &&
|
|
154
|
+
a.target.entityId === creator);
|
|
155
|
+
if (alreadyAdmin)
|
|
156
|
+
return list;
|
|
157
|
+
return [
|
|
158
|
+
...list,
|
|
159
|
+
{ kind: 'relationship', label: 'admin', target: { scope: 'entity', entityId: creator } },
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether a Record is a `_group`, in any of its type versions — the family
|
|
164
|
+
* whose roster rules apply.
|
|
165
|
+
*/
|
|
166
|
+
export function isGroupRecord(record) {
|
|
167
|
+
return baseIdOf(record.typeId) === SYSTEM_TYPES.GROUP;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Drops a snapshot's `permissions` — owner-only audit data, never served
|
|
171
|
+
* to a non-owner history reader. `entityId` (change attribution) stays.
|
|
172
|
+
* See docs/spec/versioning.md § History access.
|
|
173
|
+
*/
|
|
174
|
+
export function stripVersionPermissions(version) {
|
|
175
|
+
if (version.permissions === undefined)
|
|
176
|
+
return version;
|
|
177
|
+
const { permissions: _permissions, ...rest } = version;
|
|
178
|
+
return rest;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The tombstone a soft-deleted Record is presented as. `permissions` is
|
|
182
|
+
* retained because it decides whether the caller may undelete; history is
|
|
183
|
+
* exempt, which is why getVersions() still serves the content this
|
|
184
|
+
* withholds. See docs/spec/versioning.md § The tombstone is literal.
|
|
185
|
+
*/
|
|
186
|
+
export function tombstoneOf(record) {
|
|
187
|
+
return {
|
|
188
|
+
id: record.id,
|
|
189
|
+
typeId: record.typeId,
|
|
190
|
+
createdAt: record.createdAt,
|
|
191
|
+
updatedAt: record.updatedAt,
|
|
192
|
+
version: record.version,
|
|
193
|
+
content: {},
|
|
194
|
+
...(record.deletedAt !== undefined && { deletedAt: record.deletedAt }),
|
|
195
|
+
...(record.unlistedAt !== undefined && { unlistedAt: record.unlistedAt }),
|
|
196
|
+
...(record.permissions !== undefined && { permissions: record.permissions }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** A soft-deleted Record presented as its tombstone; anything else untouched. */
|
|
200
|
+
export const presentDeleted = (record) => record.deletedAt ? tombstoneOf(record) : record;
|
|
201
|
+
//# sourceMappingURL=record-changes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-changes.js","sourceRoot":"","sources":["../src/record-changes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record ID and clock-field validation
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The rules a caller-supplied `id`, `parentId`, `createdAt` or `updatedAt`
|
|
5
|
+
* is held to before any of them reaches storage. A client may mint its own
|
|
6
|
+
* id and backdate a record's clock, so both are untrusted input here even
|
|
7
|
+
* when the types say otherwise.
|
|
8
|
+
*
|
|
9
|
+
* See docs/spec/data-model.md § Record IDs.
|
|
10
|
+
*/
|
|
11
|
+
import type { ValidationError } from './validate.js';
|
|
12
|
+
export declare const DEFAULT_ID_TIMESTAMP_SKEW_MS: number;
|
|
13
|
+
/**
|
|
14
|
+
* The same format rule applied to a `parentId` a caller names. Separate
|
|
15
|
+
* from validateRecordId() only for its message: the id under discussion is
|
|
16
|
+
* the destination, not the record being written, and a shared message would
|
|
17
|
+
* report the wrong one. See docs/spec/data-model.md § Reparenting.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateParentId(parentId: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Format and reserved-prefix checks — full-trust context (Stack.create()).
|
|
22
|
+
* Checked before the format check: the Crockford charset already excludes
|
|
23
|
+
* "_", so a reserved-looking id (e.g. "_config") would otherwise just fail
|
|
24
|
+
* as a generic format error instead of a specific, actionable one.
|
|
25
|
+
*
|
|
26
|
+
* Throws StackQueryError, not StackValidationError: a malformed id is
|
|
27
|
+
* structurally bad input the request never gets past — it doesn't reach
|
|
28
|
+
* type-schema validation — the same reasoning that makes an undecodable
|
|
29
|
+
* pagination cursor a StackQueryError rather than a content-validation
|
|
30
|
+
* failure. See StackQueryError's doc comment.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateRecordId(id: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Validity and range check for the backdating options on unscoped
|
|
35
|
+
* Stack.create(). A Date is only as good as what the caller parsed it
|
|
36
|
+
* from, and the two failure modes both need catching here rather than
|
|
37
|
+
* downstream:
|
|
38
|
+
*
|
|
39
|
+
* - **Invalid Date** (`new Date('13/45/2020')` off a malformed import row)
|
|
40
|
+
* has a NaN getTime(), and every comparison against NaN is false — so an
|
|
41
|
+
* unchecked Invalid Date passes the updatedAt/createdAt ordering check
|
|
42
|
+
* and the id/createdAt skew check by turning them off, mints the
|
|
43
|
+
* epoch-zero ID `000000000xxx`, and persists a record whose
|
|
44
|
+
* `createdAt.toISOString()` throws RangeError in serializeRecord() —
|
|
45
|
+
* making that record, and any wire response containing it,
|
|
46
|
+
* permanently unreadable.
|
|
47
|
+
* - **Out of encodable range** — before 1970 crockford32Encode() throws a
|
|
48
|
+
* bare RangeError from deep inside the ID encoder, and past
|
|
49
|
+
* MAX_ID_TIMESTAMP the derived ID silently grows to 13 characters and
|
|
50
|
+
* fails isValidIdFormat().
|
|
51
|
+
*
|
|
52
|
+
* Checked at the door for both, as a StackValidationError naming the
|
|
53
|
+
* field. Applies whether or not an `id` is supplied: a clock field outside
|
|
54
|
+
* this range is unrepresentable regardless of where the ID came from.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateClockField(value: Date | undefined, path: string): ValidationError[];
|
|
57
|
+
/**
|
|
58
|
+
* Timestamp-prefix plausibility check, shared by callers that compare an
|
|
59
|
+
* ID's embedded millisecond against a different reference each:
|
|
60
|
+
* ScopedStack.create() against the current time for any non-backdated
|
|
61
|
+
* create (a grantee is untrusted and could otherwise mint an ID that
|
|
62
|
+
* forges its sort position, and this is the one check standing between a
|
|
63
|
+
* delegated or grantee caller and doing so), and Stack.create() — reached
|
|
64
|
+
* directly when unscoped, or via ScopedStack.create() when the requester
|
|
65
|
+
* is the owner acting alone with an explicit `createdAt` — against that
|
|
66
|
+
* `createdAt` instead (the two must agree, not silently diverge — see
|
|
67
|
+
* docs/spec/data-model.md § Record IDs). Pass null to disable.
|
|
68
|
+
*/
|
|
69
|
+
export declare function validateIdTimestampSkew(id: string, toleranceMs: number | null, referenceMs: number, referenceLabel: string): void;
|
|
70
|
+
//# sourceMappingURL=record-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-id.d.ts","sourceRoot":"","sources":["../src/record-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAOrD,eAAO,MAAM,4BAA4B,QAAsB,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAWvD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CASjD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE,CA0B3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,GACrB,IAAI,CAWN"}
|