@haverstack/core 0.29.0 → 0.30.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.
Files changed (73) hide show
  1. package/dist/adapter-entry.d.ts +1 -1
  2. package/dist/adapter-entry.d.ts.map +1 -1
  3. package/dist/adapter-entry.js +1 -1
  4. package/dist/adapter-entry.js.map +1 -1
  5. package/dist/changes.d.ts +14 -0
  6. package/dist/changes.d.ts.map +1 -1
  7. package/dist/changes.js +35 -0
  8. package/dist/changes.js.map +1 -1
  9. package/dist/errors.d.ts +173 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +208 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/grants.d.ts +84 -0
  14. package/dist/grants.d.ts.map +1 -0
  15. package/dist/grants.js +157 -0
  16. package/dist/grants.js.map +1 -0
  17. package/dist/id.d.ts +7 -5
  18. package/dist/id.d.ts.map +1 -1
  19. package/dist/id.js +17 -15
  20. package/dist/id.js.map +1 -1
  21. package/dist/identity-bindings.d.ts +13 -0
  22. package/dist/identity-bindings.d.ts.map +1 -0
  23. package/dist/identity-bindings.js +42 -0
  24. package/dist/identity-bindings.js.map +1 -0
  25. package/dist/index.d.ts +6 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/limits.d.ts +23 -0
  30. package/dist/limits.d.ts.map +1 -0
  31. package/dist/limits.js +35 -0
  32. package/dist/limits.js.map +1 -0
  33. package/dist/query-validation.d.ts +80 -0
  34. package/dist/query-validation.d.ts.map +1 -0
  35. package/dist/query-validation.js +223 -0
  36. package/dist/query-validation.js.map +1 -0
  37. package/dist/record-changes.d.ts +89 -0
  38. package/dist/record-changes.d.ts.map +1 -0
  39. package/dist/record-changes.js +201 -0
  40. package/dist/record-changes.js.map +1 -0
  41. package/dist/record-id.d.ts +70 -0
  42. package/dist/record-id.d.ts.map +1 -0
  43. package/dist/record-id.js +127 -0
  44. package/dist/record-id.js.map +1 -0
  45. package/dist/scoped-stack.d.ts +460 -0
  46. package/dist/scoped-stack.d.ts.map +1 -0
  47. package/dist/scoped-stack.js +1163 -0
  48. package/dist/scoped-stack.js.map +1 -0
  49. package/dist/stack-reads.d.ts +38 -0
  50. package/dist/stack-reads.d.ts.map +1 -0
  51. package/dist/stack-reads.js +79 -0
  52. package/dist/stack-reads.js.map +1 -0
  53. package/dist/stack.d.ts +37 -643
  54. package/dist/stack.d.ts.map +1 -1
  55. package/dist/stack.js +102 -2254
  56. package/dist/stack.js.map +1 -1
  57. package/dist/testing.d.ts.map +1 -1
  58. package/dist/testing.js +3 -1
  59. package/dist/testing.js.map +1 -1
  60. package/dist/types.d.ts +9 -2
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/types.js +8 -0
  63. package/dist/types.js.map +1 -1
  64. package/dist/wire-entry.d.ts +2 -2
  65. package/dist/wire-entry.d.ts.map +1 -1
  66. package/dist/wire-entry.js +2 -2
  67. package/dist/wire-entry.js.map +1 -1
  68. package/dist/wire-record.js +1 -1
  69. package/dist/wire-record.js.map +1 -1
  70. package/dist/wire-request.d.ts.map +1 -1
  71. package/dist/wire-request.js +3 -2
  72. package/dist/wire-request.js.map +1 -1
  73. package/package.json +1 -1
@@ -0,0 +1,223 @@
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.
14
+ * See docs/spec/data-model.md § Capability-gated filters.
15
+ */
16
+ import { StackQueryError } from './errors.js';
17
+ import { CONTENT_SEGMENT_METACHARACTERS, SEGMENT_METACHARACTER_RE } from './validate.js';
18
+ import { NATIVE_SORT_FIELDS } from './types.js';
19
+ /**
20
+ * Fail loud rather than silently widen: a filter the adapter can't honor
21
+ * would otherwise be dropped, returning an unfiltered superset presented
22
+ * as the filtered result. Shared by Stack.query() and
23
+ * APIAdapter.queryRecords(). See docs/spec/data-model.md
24
+ * § Capability-gated filters.
25
+ */
26
+ export function assertQueryCapabilities(filter, capabilities) {
27
+ const { content: reach, contentPresent, search } = capabilities.filter;
28
+ if (filter?.search && !search) {
29
+ throw new StackQueryError('Query uses filter.search, but this adapter does not declare the filter.search capability.');
30
+ }
31
+ const present = filter?.contentPresent?.length ? filter.contentPresent : undefined;
32
+ if (!filter?.content && !present)
33
+ return;
34
+ if (reach === 'none') {
35
+ throw new StackQueryError(`Query uses ${present && !filter?.content ? 'filter.contentPresent' : 'filter.content'}, ` +
36
+ 'but this adapter declares filter.content: "none".');
37
+ }
38
+ if (present && !contentPresent) {
39
+ throw new StackQueryError('Query uses filter.contentPresent, but this adapter does not declare the ' +
40
+ 'filter.contentPresent capability.');
41
+ }
42
+ for (const key of [...Object.keys(filter?.content ?? {}), ...(present ?? [])]) {
43
+ if (parseContentFilterKey(key).length > 1 && reach !== 'path') {
44
+ throw new StackQueryError(`Query uses the nested content path "${key}", but this adapter declares ` +
45
+ `filter.content: "${reach}".`);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Whether a single-segment content filter can be pushed down to the
51
+ * adapter. Callers that read a content field pair this with an in-memory
52
+ * predicate over the wider result, so the query stays correct against an
53
+ * adapter that reaches no content at all — the rung comparison lives here
54
+ * rather than at each of them.
55
+ */
56
+ export function filtersContent(features) {
57
+ return features.filter.content !== 'none';
58
+ }
59
+ /** The longest path both SQLite engines can execute — see the spec link below. */
60
+ const MAX_CONTENT_PATH_SEGMENTS = 32;
61
+ /**
62
+ * Split a content filter key into path segments. Write-time validation
63
+ * keeps stored field names free of the path metacharacters, but a filter
64
+ * arrives from a request body and has made no such promise, so the same
65
+ * rule is enforced here. See docs/spec/data-model.md § Content field names.
66
+ */
67
+ export function parseContentFilterKey(key) {
68
+ const segments = key.split('.');
69
+ // A SQLite adapter spends two json_each joins per segment against a
70
+ // 64-table join limit, so a path past the cap is one the engine could
71
+ // not execute. See docs/spec/data-model.md § Nested content paths.
72
+ if (segments.length > MAX_CONTENT_PATH_SEGMENTS) {
73
+ throw new StackQueryError(`Invalid content filter path "${key}": at most ${MAX_CONTENT_PATH_SEGMENTS} segments.`);
74
+ }
75
+ for (const segment of segments) {
76
+ if (segment === '') {
77
+ throw new StackQueryError(`Invalid content filter path "${key}": a path segment cannot be empty.`);
78
+ }
79
+ if (SEGMENT_METACHARACTER_RE.test(segment)) {
80
+ throw new StackQueryError(`Invalid content filter path "${key}": a segment cannot contain any of ` +
81
+ `${CONTENT_SEGMENT_METACHARACTERS.join(' ')}.`);
82
+ }
83
+ }
84
+ return segments;
85
+ }
86
+ /**
87
+ * The only sort fields any adapter maps; anything else is a caller error.
88
+ * Narrowed against the one array NativeSortField is derived from, so this
89
+ * gate — which every query passes through before an adapter sees it —
90
+ * can't be the copy that a fourth native column is missing from.
91
+ */
92
+ const VALID_SORT_FIELDS = new Set(NATIVE_SORT_FIELDS);
93
+ /** The only two sort directions; see assertValidSort. */
94
+ const VALID_SORT_DIRECTIONS = new Set(['asc', 'desc']);
95
+ /**
96
+ * Reject a sort whose field or direction is outside the closed set the
97
+ * types promise. `QuerySort` is typed `'asc' | 'desc'`, but a type is not a
98
+ * runtime guard: a server mapping `?direction=` onto a query, or a
99
+ * delegated app calling query(), supplies a raw string. A SQLite record
100
+ * adapter interpolates the direction straight into `ORDER BY`, so an
101
+ * unvalidated value there is a SQL-injection sink reachable from every
102
+ * untrusted caller. Validating in the invariant layer — the same reason
103
+ * emission and _config protection live here — means no adapter can forget
104
+ * it. See docs/spec/data-model.md § Sorting and pagination.
105
+ */
106
+ export function assertValidSort(sort) {
107
+ if (!sort)
108
+ return;
109
+ if (sort.field !== undefined && sort.contentField !== undefined) {
110
+ throw new StackQueryError('A sort names either a native field or a content field, never both.');
111
+ }
112
+ if (sort.field !== undefined && !VALID_SORT_FIELDS.has(sort.field)) {
113
+ throw new StackQueryError(`Invalid sort field "${sort.field}": expected one of ${NATIVE_SORT_FIELDS.join(', ')}.`);
114
+ }
115
+ if (sort.contentField !== undefined) {
116
+ // Parsed by the same rule a filter key is, so a name a filter could
117
+ // never address is not one a sort can either — then held to one
118
+ // segment, because a value inside an array or object has no single
119
+ // position to order its record by (docs/spec/data-model.md
120
+ // § Sorting by a content field).
121
+ if (parseContentFilterKey(sort.contentField).length > 1) {
122
+ throw new StackQueryError(`Invalid sort content field "${sort.contentField}": sorting reaches top-level fields only.`);
123
+ }
124
+ }
125
+ if (sort.direction !== undefined && !VALID_SORT_DIRECTIONS.has(sort.direction)) {
126
+ throw new StackQueryError(`Invalid sort direction "${sort.direction}": expected "asc" or "desc".`);
127
+ }
128
+ }
129
+ /**
130
+ * Fail loud rather than silently reorder: an adapter that can't honor the
131
+ * requested sort would otherwise answer in some other order, which a
132
+ * caller paging a bounded window has no way to notice. The companion to
133
+ * assertQueryCapabilities(), split from it because a sort is not a filter
134
+ * — see docs/spec/data-model.md § Capability-gated filters.
135
+ */
136
+ export function assertSortCapability(sort, capabilities) {
137
+ if (!sort)
138
+ return;
139
+ if (sort.contentField !== undefined) {
140
+ if (!capabilities.sort.contentField) {
141
+ throw new StackQueryError('Query uses sort.contentField, but this adapter does not declare the sort.contentField ' +
142
+ 'capability.');
143
+ }
144
+ return;
145
+ }
146
+ const field = sort.field ?? 'createdAt';
147
+ if (!capabilities.sort.fields.includes(field)) {
148
+ throw new StackQueryError(`Query sorts by "${field}", which this adapter does not declare in sort.fields.`);
149
+ }
150
+ }
151
+ /** The identifier spaces a relationship target may name. */
152
+ const TARGET_SCOPES = new Set(['record', 'entity', 'external']);
153
+ /**
154
+ * Collect what makes a relationship target malformed. Absence is
155
+ * meaningful on `stackUrl` and an external `id` — this stack, and the
156
+ * whole namespace — so every part that names something must be non-empty:
157
+ * an empty string stores and matches as though it were absent.
158
+ * See docs/spec/data-model.md § Relationship targets.
159
+ */
160
+ function targetErrors(target, path, opts = {}) {
161
+ const fail = (message) => [{ path, message }];
162
+ if (!target || typeof target !== 'object')
163
+ return fail('A relationship target must be an object.');
164
+ if (!TARGET_SCOPES.has(target.scope)) {
165
+ return fail(`Unknown relationship target scope "${target.scope}": expected "record", "entity" or "external".`);
166
+ }
167
+ if (target.scope === 'record') {
168
+ if (!target.recordId)
169
+ return fail('A record target requires a non-empty recordId.');
170
+ if (target.stackUrl !== undefined && !target.stackUrl) {
171
+ return fail("A record target's stackUrl must be non-empty; omit it to name this stack.");
172
+ }
173
+ return [];
174
+ }
175
+ if (target.scope === 'entity') {
176
+ return target.entityId ? [] : fail('An entity target requires a non-empty entityId.');
177
+ }
178
+ if (!target.ns)
179
+ return fail('An external target requires a non-empty ns.');
180
+ if (target.id === undefined) {
181
+ return opts.externalIdOptional ? [] : fail('An external target requires an id.');
182
+ }
183
+ return target.id ? [] : fail("An external target's id must be non-empty when present.");
184
+ }
185
+ /**
186
+ * Reject a relationship target outside the closed set the types promise.
187
+ * A discriminated union is not a runtime guard — a server mapping a
188
+ * request body onto an association supplies raw JSON — and an
189
+ * unrecognized scope would otherwise be stored under the one arm that
190
+ * names a Record in this stack. See docs/spec/data-model.md
191
+ * § Relationship targets.
192
+ */
193
+ export function validateAssociation(association, path = 'association') {
194
+ if (association?.kind !== 'relationship')
195
+ return [];
196
+ return targetErrors(association.target, `${path}.target`);
197
+ }
198
+ /** validateAssociation() over a create's `associations` array. */
199
+ export function validateAssociations(associations, path = 'associations') {
200
+ return (associations ?? []).flatMap((a, i) => validateAssociation(a, `${path}[${i}]`));
201
+ }
202
+ /**
203
+ * Reject a relationship filter that names neither a label nor a target,
204
+ * or whose target is malformed. `RelatedToFilter` promises one half is
205
+ * always present; without the runtime check a filter decoded from a
206
+ * request could arrive empty and match every record carrying any
207
+ * relationship. See docs/spec/data-model.md § Filter.
208
+ */
209
+ export function assertValidRelatedTo(relatedTo) {
210
+ if (!relatedTo)
211
+ return;
212
+ if (relatedTo.label === undefined && relatedTo.target === undefined) {
213
+ throw new StackQueryError('filter.relatedTo must name a label, a target, or both — "any relationship at all" is not a filter.');
214
+ }
215
+ if (relatedTo.target === undefined)
216
+ return;
217
+ const errors = targetErrors(relatedTo.target, 'filter.relatedTo.target', {
218
+ externalIdOptional: true,
219
+ });
220
+ if (errors.length > 0)
221
+ throw new StackQueryError(errors[0].message);
222
+ }
223
+ //# 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"}