@haverstack/core 0.28.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.
- package/README.md +9 -2
- package/dist/access.d.ts +20 -0
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +26 -0
- package/dist/access.js.map +1 -1
- package/dist/adapter-entry.d.ts +1 -1
- 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 +15 -2
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +62 -6
- package/dist/changes.js.map +1 -1
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +1 -4
- package/dist/combine.js.map +1 -1
- package/dist/errors.d.ts +173 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +208 -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/merge.d.ts +1 -1
- package/dist/merge.js +2 -2
- package/dist/merge.js.map +1 -1
- package/dist/query-validation.d.ts +80 -0
- package/dist/query-validation.d.ts.map +1 -0
- package/dist/query-validation.js +223 -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/schema.d.ts.map +1 -1
- package/dist/schema.js +56 -5
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack.d.ts +460 -0
- package/dist/scoped-stack.d.ts.map +1 -0
- package/dist/scoped-stack.js +1163 -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 +108 -700
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +235 -2286
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts +3 -14
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +30 -42
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +107 -30
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +31 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +150 -3
- package/dist/validate.js.map +1 -1
- package/dist/wire-entry.d.ts +3 -3
- package/dist/wire-entry.d.ts.map +1 -1
- package/dist/wire-entry.js +3 -3
- package/dist/wire-entry.js.map +1 -1
- package/dist/wire-record.d.ts +16 -1
- package/dist/wire-record.d.ts.map +1 -1
- package/dist/wire-record.js +57 -1
- package/dist/wire-record.js.map +1 -1
- package/dist/wire-request.d.ts.map +1 -1
- package/dist/wire-request.js +3 -2
- package/dist/wire-request.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
* Storage adapters are published as separate packages:
|
|
15
15
|
* @haverstack/adapter-sqlite
|
|
16
16
|
*/
|
|
17
|
-
export { Stack
|
|
18
|
-
export
|
|
19
|
-
export type {
|
|
20
|
-
export {
|
|
17
|
+
export { Stack } from './stack.js';
|
|
18
|
+
export { ScopedStack } from './scoped-stack.js';
|
|
19
|
+
export type { StackClient, CreateRecordOptions, BackdatableCreateRecordOptions, StackOptions, GetRecordOptions, DeleteRecordOptions, IfVersionOptions, CollectAttachmentGarbageOptions, CollectAttachmentGarbageResult, } from './stack.js';
|
|
20
|
+
export { StackError, StackValidationError, StackMigrationError, StackPermissionError, StackNotFoundError, StackConflictError, StackVersionConflictError, StackQueryError, StackSchemaDriftError, StackPayloadTooLargeError, StackTimeoutError, StackClosedError, StackRelayScopeError, } from './errors.js';
|
|
21
|
+
export type { StackErrorCode } from './errors.js';
|
|
22
|
+
export type { RecordId, TypeId, FileId, EntityId, AttachmentContent, StackRecord, RecordChanges, RecordVersion, ActorOptions, ChangeKind, ChangeOp, ChangeActor, RecordChange, ChangeFilter, SubscribeOptions, Unsubscribe, ExpectedVersionOptions, SnapshotOptions, StackType, TypeSchema, FieldDef, ScalarFieldDef, ArrayFieldDef, ObjectFieldDef, ScalarFieldKind, Association, TagAssociation, AttachmentAssociation, RelationshipAssociation, RelationshipTarget, RecordTarget, EntityTarget, ExternalTarget, Permission, StackQuery, RecordFilter, RelatedToFilter, RelationshipTargetPattern, QuerySort, NativeSortField, ContentFilterReach, QueryResult, DateRange, Migration, MigrationFn, StackAdapter, StackFeatures, TokenSession, EntityContent, AppContent, GroupContent, GrantAction, GrantContent, ConfigContent, } from './types.js';
|
|
23
|
+
export { SYSTEM_TYPES, NATIVE_SORT_FIELDS } from './types.js';
|
|
21
24
|
export { generateId } from './id.js';
|
|
22
25
|
export { hashSchema, isCompatible } from './schema.js';
|
|
23
26
|
export type { SchemaDriftViolation } from './schema.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,8BAA8B,EAC9B,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,YAAY,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,aAAa,EACb,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,SAAS,EACT,UAAU,EACV,QAAQ,EACR,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,UAAU,EACV,UAAU,EACV,YAAY,EACZ,eAAe,EACf,yBAAyB,EACzB,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAM9D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* @haverstack/adapter-sqlite
|
|
16
16
|
*/
|
|
17
17
|
// Core class and client interface
|
|
18
|
-
export { Stack
|
|
19
|
-
export {
|
|
18
|
+
export { Stack } from './stack.js';
|
|
19
|
+
export { ScopedStack } from './scoped-stack.js';
|
|
20
|
+
// The error taxonomy
|
|
21
|
+
export { StackError, StackValidationError, StackMigrationError, StackPermissionError, StackNotFoundError, StackConflictError, StackVersionConflictError, StackQueryError, StackSchemaDriftError, StackPayloadTooLargeError, StackTimeoutError, StackClosedError, StackRelayScopeError, } from './errors.js';
|
|
22
|
+
export { SYSTEM_TYPES, NATIVE_SORT_FIELDS } from './types.js';
|
|
20
23
|
// Utilities
|
|
21
24
|
// No in-repo caller: client-minted IDs are the default per spec, and an app
|
|
22
25
|
// needing the ID before the write round-trips has to mint one itself.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,kCAAkC;AAClC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,kCAAkC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAahD,qBAAqB;AACrB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AA6DrB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE9D,YAAY;AAEZ,4EAA4E;AAC5E,sEAAsE;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared-ceiling pre-checks
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Client-side guards against an adapter's declared `limits`. Local
|
|
5
|
+
* adapters declare `null` and skip them entirely; only a server names a
|
|
6
|
+
* ceiling, and its own request-size limit stays authoritative — these just
|
|
7
|
+
* spare an app the round trip and give it a typed failure instead of a 413
|
|
8
|
+
* it has to interpret.
|
|
9
|
+
*
|
|
10
|
+
* See docs/spec/wire-format.md § Request size limits.
|
|
11
|
+
*/
|
|
12
|
+
/** Shared by Stack.putAttachment() and ScopedStack.putAttachment(). */
|
|
13
|
+
export declare function assertAttachmentSize(byteLength: number, attachmentBytes: number | null): void;
|
|
14
|
+
/**
|
|
15
|
+
* The content half of the same pre-check, on Stack.create() and
|
|
16
|
+
* Stack.mutate(). Local adapters declare `limits.contentBytes: null` and skip
|
|
17
|
+
* the serialization entirely; only a server declares a ceiling, and its
|
|
18
|
+
* own request-size limit stays authoritative — this just spares an app the
|
|
19
|
+
* round trip and gives it a typed failure instead of a 413 it has to
|
|
20
|
+
* interpret. See docs/spec/wire-format.md § Request size limits.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertContentSize(content: Record<string, unknown>, contentBytes: number | null, what: 'Content' | 'Patch'): void;
|
|
23
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAM7F;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,IAAI,EAAE,SAAS,GAAG,OAAO,GACxB,IAAI,CAQN"}
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared-ceiling pre-checks
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Client-side guards against an adapter's declared `limits`. Local
|
|
5
|
+
* adapters declare `null` and skip them entirely; only a server names a
|
|
6
|
+
* ceiling, and its own request-size limit stays authoritative — these just
|
|
7
|
+
* spare an app the round trip and give it a typed failure instead of a 413
|
|
8
|
+
* it has to interpret.
|
|
9
|
+
*
|
|
10
|
+
* See docs/spec/wire-format.md § Request size limits.
|
|
11
|
+
*/
|
|
12
|
+
import { StackPayloadTooLargeError } from './errors.js';
|
|
13
|
+
/** Shared by Stack.putAttachment() and ScopedStack.putAttachment(). */
|
|
14
|
+
export function assertAttachmentSize(byteLength, attachmentBytes) {
|
|
15
|
+
if (attachmentBytes !== null && byteLength > attachmentBytes) {
|
|
16
|
+
throw new StackPayloadTooLargeError(`Attachment (${byteLength} bytes) exceeds the ${attachmentBytes}-byte limit.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The content half of the same pre-check, on Stack.create() and
|
|
21
|
+
* Stack.mutate(). Local adapters declare `limits.contentBytes: null` and skip
|
|
22
|
+
* the serialization entirely; only a server declares a ceiling, and its
|
|
23
|
+
* own request-size limit stays authoritative — this just spares an app the
|
|
24
|
+
* round trip and gives it a typed failure instead of a 413 it has to
|
|
25
|
+
* interpret. See docs/spec/wire-format.md § Request size limits.
|
|
26
|
+
*/
|
|
27
|
+
export function assertContentSize(content, contentBytes, what) {
|
|
28
|
+
if (contentBytes === null)
|
|
29
|
+
return;
|
|
30
|
+
const byteLength = new TextEncoder().encode(JSON.stringify(content)).length;
|
|
31
|
+
if (byteLength > contentBytes) {
|
|
32
|
+
throw new StackPayloadTooLargeError(`${what} (${byteLength} bytes) exceeds the ${contentBytes}-byte limit.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExD,uEAAuE;AACvE,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,eAA8B;IACrF,IAAI,eAAe,KAAK,IAAI,IAAI,UAAU,GAAG,eAAe,EAAE,CAAC;QAC7D,MAAM,IAAI,yBAAyB,CACjC,eAAe,UAAU,uBAAuB,eAAe,cAAc,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAgC,EAChC,YAA2B,EAC3B,IAAyB;IAEzB,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO;IAClC,MAAM,UAAU,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC5E,IAAI,UAAU,GAAG,YAAY,EAAE,CAAC;QAC9B,MAAM,IAAI,yBAAyB,CACjC,GAAG,IAAI,KAAK,UAAU,uBAAuB,YAAY,cAAc,CACxE,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/dist/merge.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* -------------------------------------------------------
|
|
4
4
|
* Shallow merge-patch semantics for record content: a field set to `null`
|
|
5
5
|
* removes it, any other value replaces it, and omitted fields are retained.
|
|
6
|
-
* Shared by Stack.
|
|
6
|
+
* Shared by Stack.mutate() (for client-side validation) and every
|
|
7
7
|
* StackRecordAdapter's patchContent() implementation, so a patch produces
|
|
8
8
|
* the same merged content everywhere it's applied.
|
|
9
9
|
*/
|
package/dist/merge.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* -------------------------------------------------------
|
|
4
4
|
* Shallow merge-patch semantics for record content: a field set to `null`
|
|
5
5
|
* removes it, any other value replaces it, and omitted fields are retained.
|
|
6
|
-
* Shared by Stack.
|
|
6
|
+
* Shared by Stack.mutate() (for client-side validation) and every
|
|
7
7
|
* StackRecordAdapter's patchContent() implementation, so a patch produces
|
|
8
8
|
* the same merged content everywhere it's applied.
|
|
9
9
|
*/
|
|
@@ -22,7 +22,7 @@ export function applyMergePatch(content, patch) {
|
|
|
22
22
|
// the same key is read back, so both write paths agree. The spread
|
|
23
23
|
// above is already safe (it copies data properties directly).
|
|
24
24
|
//
|
|
25
|
-
// Stack.create()/
|
|
25
|
+
// Stack.create()/Stack.mutate() reject the reserved keys outright (see
|
|
26
26
|
// validateReservedKeys); this is the backstop for adapters that call
|
|
27
27
|
// this directly, and for the day someone deepens the merge.
|
|
28
28
|
Object.defineProperty(merged, key, {
|
package/dist/merge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,UAAU,eAAe,CAC7B,OAAgC,EAChC,KAAqC;IAErC,MAAM,MAAM,GAA4B,EAAE,GAAG,OAAO,EAAE,CAAC;IACvD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,qEAAqE;YACrE,oEAAoE;YACpE,kEAAkE;YAClE,mEAAmE;YACnE,qEAAqE;YACrE,mEAAmE;YACnE,8DAA8D;YAC9D,EAAE;YACF,
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,UAAU,eAAe,CAC7B,OAAgC,EAChC,KAAqC;IAErC,MAAM,MAAM,GAA4B,EAAE,GAAG,OAAO,EAAE,CAAC;IACvD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,qEAAqE;YACrE,oEAAoE;YACpE,kEAAkE;YAClE,mEAAmE;YACnE,qEAAqE;YACrE,mEAAmE;YACnE,8DAA8D;YAC9D,EAAE;YACF,uEAAuE;YACvE,qEAAqE;YACrE,4DAA4D;YAC5D,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE;gBACjC,KAAK;gBACL,QAAQ,EAAE,IAAI;gBACd,UAAU,EAAE,IAAI;gBAChB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
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 type { ValidationError } from './validate.js';
|
|
17
|
+
import type { Association, QuerySort, RecordFilter, StackFeatures } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Fail loud rather than silently widen: a filter the adapter can't honor
|
|
20
|
+
* would otherwise be dropped, returning an unfiltered superset presented
|
|
21
|
+
* as the filtered result. Shared by Stack.query() and
|
|
22
|
+
* APIAdapter.queryRecords(). See docs/spec/data-model.md
|
|
23
|
+
* § Capability-gated filters.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertQueryCapabilities(filter: RecordFilter | undefined, capabilities: Pick<StackFeatures, 'filter'>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a single-segment content filter can be pushed down to the
|
|
28
|
+
* adapter. Callers that read a content field pair this with an in-memory
|
|
29
|
+
* predicate over the wider result, so the query stays correct against an
|
|
30
|
+
* adapter that reaches no content at all — the rung comparison lives here
|
|
31
|
+
* rather than at each of them.
|
|
32
|
+
*/
|
|
33
|
+
export declare function filtersContent(features: Pick<StackFeatures, 'filter'>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Split a content filter key into path segments. Write-time validation
|
|
36
|
+
* keeps stored field names free of the path metacharacters, but a filter
|
|
37
|
+
* arrives from a request body and has made no such promise, so the same
|
|
38
|
+
* rule is enforced here. See docs/spec/data-model.md § Content field names.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseContentFilterKey(key: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Reject a sort whose field or direction is outside the closed set the
|
|
43
|
+
* types promise. `QuerySort` is typed `'asc' | 'desc'`, but a type is not a
|
|
44
|
+
* runtime guard: a server mapping `?direction=` onto a query, or a
|
|
45
|
+
* delegated app calling query(), supplies a raw string. A SQLite record
|
|
46
|
+
* adapter interpolates the direction straight into `ORDER BY`, so an
|
|
47
|
+
* unvalidated value there is a SQL-injection sink reachable from every
|
|
48
|
+
* untrusted caller. Validating in the invariant layer — the same reason
|
|
49
|
+
* emission and _config protection live here — means no adapter can forget
|
|
50
|
+
* it. See docs/spec/data-model.md § Sorting and pagination.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertValidSort(sort: QuerySort | undefined): void;
|
|
53
|
+
/**
|
|
54
|
+
* Fail loud rather than silently reorder: an adapter that can't honor the
|
|
55
|
+
* requested sort would otherwise answer in some other order, which a
|
|
56
|
+
* caller paging a bounded window has no way to notice. The companion to
|
|
57
|
+
* assertQueryCapabilities(), split from it because a sort is not a filter
|
|
58
|
+
* — see docs/spec/data-model.md § Capability-gated filters.
|
|
59
|
+
*/
|
|
60
|
+
export declare function assertSortCapability(sort: QuerySort | undefined, capabilities: Pick<StackFeatures, 'sort'>): void;
|
|
61
|
+
/**
|
|
62
|
+
* Reject a relationship target outside the closed set the types promise.
|
|
63
|
+
* A discriminated union is not a runtime guard — a server mapping a
|
|
64
|
+
* request body onto an association supplies raw JSON — and an
|
|
65
|
+
* unrecognized scope would otherwise be stored under the one arm that
|
|
66
|
+
* names a Record in this stack. See docs/spec/data-model.md
|
|
67
|
+
* § Relationship targets.
|
|
68
|
+
*/
|
|
69
|
+
export declare function validateAssociation(association: Association, path?: string): ValidationError[];
|
|
70
|
+
/** validateAssociation() over a create's `associations` array. */
|
|
71
|
+
export declare function validateAssociations(associations: Association[] | undefined, path?: string): ValidationError[];
|
|
72
|
+
/**
|
|
73
|
+
* Reject a relationship filter that names neither a label nor a target,
|
|
74
|
+
* or whose target is malformed. `RelatedToFilter` promises one half is
|
|
75
|
+
* always present; without the runtime check a filter decoded from a
|
|
76
|
+
* request could arrive empty and match every record carrying any
|
|
77
|
+
* relationship. See docs/spec/data-model.md § Filter.
|
|
78
|
+
*/
|
|
79
|
+
export declare function assertValidRelatedTo(relatedTo: RecordFilter['relatedTo']): void;
|
|
80
|
+
//# sourceMappingURL=query-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-validation.d.ts","sourceRoot":"","sources":["../src/query-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,YAAY,EAGZ,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,YAAY,GAAG,SAAS,EAChC,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAC1C,IAAI,CA6BN;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,OAAO,CAE/E;AAKD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwB3D;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CA2BjE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,GAAG,SAAS,EAC3B,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GACxC,IAAI,CAiBN;AA0CD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,WAAW,EACxB,IAAI,SAAgB,GACnB,eAAe,EAAE,CAGnB;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,WAAW,EAAE,GAAG,SAAS,EACvC,IAAI,SAAiB,GACpB,eAAe,EAAE,CAEnB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,IAAI,CAY/E"}
|
|
@@ -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"}
|