@haverstack/core 0.41.0 → 0.43.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 +124 -64
- package/dist/access.d.ts +43 -47
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +66 -101
- package/dist/access.js.map +1 -1
- package/dist/{combine.d.ts → adapter/combine.d.ts} +8 -1
- package/dist/adapter/combine.d.ts.map +1 -0
- package/dist/{combine.js → adapter/combine.js} +18 -3
- package/dist/adapter/combine.js.map +1 -0
- package/dist/adapter/index.d.ts +19 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +16 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter/reach.d.ts +14 -0
- package/dist/adapter/reach.d.ts.map +1 -0
- package/dist/adapter/reach.js +31 -0
- package/dist/adapter/reach.js.map +1 -0
- package/dist/{sort.d.ts → adapter/sort.d.ts} +20 -20
- package/dist/adapter/sort.d.ts.map +1 -0
- package/dist/{sort.js → adapter/sort.js} +37 -27
- package/dist/adapter/sort.js.map +1 -0
- package/dist/associations/identity.d.ts +69 -0
- package/dist/associations/identity.d.ts.map +1 -0
- package/dist/associations/identity.js +174 -0
- package/dist/associations/identity.js.map +1 -0
- package/dist/associations/validation.d.ts +83 -0
- package/dist/associations/validation.d.ts.map +1 -0
- package/dist/associations/validation.js +312 -0
- package/dist/associations/validation.js.map +1 -0
- package/dist/attachment-records.d.ts +48 -0
- package/dist/attachment-records.d.ts.map +1 -0
- package/dist/attachment-records.js +52 -0
- package/dist/attachment-records.js.map +1 -0
- package/dist/collection.d.ts +178 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +397 -0
- package/dist/collection.js.map +1 -0
- package/dist/content-match.d.ts +25 -0
- package/dist/content-match.d.ts.map +1 -0
- package/dist/content-match.js +51 -0
- package/dist/content-match.js.map +1 -0
- package/dist/content-translation.d.ts +42 -0
- package/dist/content-translation.d.ts.map +1 -0
- package/dist/content-translation.js +108 -0
- package/dist/content-translation.js.map +1 -0
- package/dist/did/did.d.ts.map +1 -0
- package/dist/{did.js → did/did.js} +3 -4
- package/dist/did/did.js.map +1 -0
- package/dist/{did-entry.d.ts → did/index.d.ts} +1 -1
- package/dist/did/index.d.ts.map +1 -0
- package/dist/{did-entry.js → did/index.js} +3 -3
- package/dist/did/index.js.map +1 -0
- package/dist/errors.d.ts +54 -66
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +58 -62
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +20 -44
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +69 -151
- package/dist/grants.js.map +1 -1
- package/dist/id.d.ts +13 -31
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +13 -31
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +3 -6
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +6 -10
- package/dist/install.js.map +1 -1
- package/dist/lazy.d.ts +13 -0
- package/dist/lazy.d.ts.map +1 -0
- package/dist/lazy.js +27 -0
- package/dist/lazy.js.map +1 -0
- package/dist/merge.d.ts +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +6 -12
- package/dist/merge.js.map +1 -1
- package/dist/migration-steps.d.ts +102 -0
- package/dist/migration-steps.d.ts.map +1 -0
- package/dist/migration-steps.js +293 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/query-validation.d.ts +68 -121
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +214 -380
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +11 -150
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +14 -305
- package/dist/record-changes.js.map +1 -1
- package/dist/schema.d.ts +18 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +59 -30
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack/authority-snapshot.d.ts +37 -0
- package/dist/scoped-stack/authority-snapshot.d.ts.map +1 -0
- package/dist/scoped-stack/authority-snapshot.js +49 -0
- package/dist/scoped-stack/authority-snapshot.js.map +1 -0
- package/dist/scoped-stack/authority.d.ts +169 -0
- package/dist/scoped-stack/authority.d.ts.map +1 -0
- package/dist/scoped-stack/authority.js +408 -0
- package/dist/scoped-stack/authority.js.map +1 -0
- package/dist/scoped-stack/family-policy.d.ts +59 -0
- package/dist/scoped-stack/family-policy.d.ts.map +1 -0
- package/dist/scoped-stack/family-policy.js +68 -0
- package/dist/scoped-stack/family-policy.js.map +1 -0
- package/dist/scoped-stack/scoped-stack.d.ts +318 -0
- package/dist/scoped-stack/scoped-stack.d.ts.map +1 -0
- package/dist/scoped-stack/scoped-stack.js +695 -0
- package/dist/scoped-stack/scoped-stack.js.map +1 -0
- package/dist/scoped-stack/subscription.d.ts +36 -0
- package/dist/scoped-stack/subscription.d.ts.map +1 -0
- package/dist/scoped-stack/subscription.js +69 -0
- package/dist/scoped-stack/subscription.js.map +1 -0
- package/dist/stack/arguments.d.ts +27 -0
- package/dist/stack/arguments.d.ts.map +1 -0
- package/dist/stack/arguments.js +53 -0
- package/dist/stack/arguments.js.map +1 -0
- package/dist/stack/attachments.d.ts +24 -0
- package/dist/stack/attachments.d.ts.map +1 -0
- package/dist/stack/attachments.js +113 -0
- package/dist/stack/attachments.js.map +1 -0
- package/dist/{changes.d.ts → stack/changes.d.ts} +30 -72
- package/dist/stack/changes.d.ts.map +1 -0
- package/dist/{changes.js → stack/changes.js} +40 -74
- package/dist/stack/changes.js.map +1 -0
- package/dist/stack/client.d.ts +247 -0
- package/dist/stack/client.d.ts.map +1 -0
- package/dist/stack/client.js +9 -0
- package/dist/stack/client.js.map +1 -0
- package/dist/stack/identity-bindings.d.ts.map +1 -0
- package/dist/{identity-bindings.js → stack/identity-bindings.js} +5 -14
- package/dist/stack/identity-bindings.js.map +1 -0
- package/dist/stack/install-app.d.ts +22 -0
- package/dist/stack/install-app.d.ts.map +1 -0
- package/dist/stack/install-app.js +262 -0
- package/dist/stack/install-app.js.map +1 -0
- package/dist/stack/integrity.d.ts +94 -0
- package/dist/stack/integrity.d.ts.map +1 -0
- package/dist/stack/integrity.js +268 -0
- package/dist/stack/integrity.js.map +1 -0
- package/dist/{limits.d.ts → stack/limits.d.ts} +2 -5
- package/dist/stack/limits.d.ts.map +1 -0
- package/dist/{limits.js → stack/limits.js} +3 -6
- package/dist/stack/limits.js.map +1 -0
- package/dist/stack/migrations.d.ts +59 -0
- package/dist/stack/migrations.d.ts.map +1 -0
- package/dist/stack/migrations.js +141 -0
- package/dist/stack/migrations.js.map +1 -0
- package/dist/stack/reads.d.ts +69 -0
- package/dist/stack/reads.d.ts.map +1 -0
- package/dist/stack/reads.js +141 -0
- package/dist/stack/reads.js.map +1 -0
- package/dist/stack/record-id.d.ts +35 -0
- package/dist/stack/record-id.d.ts.map +1 -0
- package/dist/stack/record-id.js +82 -0
- package/dist/stack/record-id.js.map +1 -0
- package/dist/stack/stack.d.ts +444 -0
- package/dist/stack/stack.d.ts.map +1 -0
- package/dist/stack/stack.js +1339 -0
- package/dist/stack/stack.js.map +1 -0
- package/dist/stack/system-types.d.ts +16 -0
- package/dist/stack/system-types.d.ts.map +1 -0
- package/dist/stack/system-types.js +111 -0
- package/dist/stack/system-types.js.map +1 -0
- package/dist/stack/type-grants.d.ts +20 -0
- package/dist/stack/type-grants.d.ts.map +1 -0
- package/dist/stack/type-grants.js +69 -0
- package/dist/stack/type-grants.js.map +1 -0
- package/dist/stack/write-expectation.d.ts +79 -0
- package/dist/stack/write-expectation.d.ts.map +1 -0
- package/dist/stack/write-expectation.js +71 -0
- package/dist/stack/write-expectation.js.map +1 -0
- package/dist/stack/write-gate.d.ts +23 -0
- package/dist/stack/write-gate.d.ts.map +1 -0
- package/dist/stack/write-gate.js +17 -0
- package/dist/stack/write-gate.js.map +1 -0
- package/dist/testing.d.ts +44 -33
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +180 -176
- package/dist/testing.js.map +1 -1
- package/dist/type-handle.d.ts +67 -78
- package/dist/type-handle.d.ts.map +1 -1
- package/dist/type-handle.js +44 -111
- package/dist/type-handle.js.map +1 -1
- package/dist/types/adapter.d.ts +282 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +5 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/associations.d.ts +140 -0
- package/dist/types/associations.d.ts.map +1 -0
- package/dist/types/associations.js +6 -0
- package/dist/types/associations.js.map +1 -0
- package/dist/types/capabilities.d.ts +89 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +5 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/events.d.ts +150 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +5 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/ids.d.ts +43 -0
- package/dist/types/ids.d.ts.map +1 -0
- package/dist/types/ids.js +5 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +19 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/journal.d.ts +47 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +5 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/query.d.ts +185 -0
- package/dist/types/query.d.ts.map +1 -0
- package/dist/types/query.js +10 -0
- package/dist/types/query.js.map +1 -0
- package/dist/types/records.d.ts +76 -0
- package/dist/types/records.d.ts.map +1 -0
- package/dist/types/records.js +5 -0
- package/dist/types/records.js.map +1 -0
- package/dist/types/schema.d.ts +74 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +5 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/system.d.ts +161 -0
- package/dist/types/system.d.ts.map +1 -0
- package/dist/types/system.js +32 -0
- package/dist/types/system.js.map +1 -0
- package/dist/types/tokens.d.ts +39 -0
- package/dist/types/tokens.d.ts.map +1 -0
- package/dist/types/tokens.js +5 -0
- package/dist/types/tokens.js.map +1 -0
- package/dist/validate.d.ts +31 -70
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +52 -92
- package/dist/validate.js.map +1 -1
- package/dist/{attachment-download.d.ts → wire/attachment-download.d.ts} +0 -57
- package/dist/wire/attachment-download.d.ts.map +1 -0
- package/dist/{attachment-download.js → wire/attachment-download.js} +0 -61
- package/dist/wire/attachment-download.js.map +1 -0
- package/dist/{auth.d.ts → wire/auth.d.ts} +5 -8
- package/dist/wire/auth.d.ts.map +1 -0
- package/dist/{auth.js → wire/auth.js} +5 -8
- package/dist/wire/auth.js.map +1 -0
- package/dist/{wire-body.d.ts → wire/body.d.ts} +35 -12
- package/dist/wire/body.d.ts.map +1 -0
- package/dist/{wire-body.js → wire/body.js} +74 -13
- package/dist/wire/body.js.map +1 -0
- package/dist/{wire-entry.d.ts → wire/index.d.ts} +12 -12
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/{wire-entry.js → wire/index.js} +13 -17
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/record.d.ts +31 -0
- package/dist/wire/record.d.ts.map +1 -0
- package/dist/{wire-record.js → wire/record.js} +7 -67
- package/dist/wire/record.js.map +1 -0
- package/dist/{wire-request.d.ts → wire/request.d.ts} +32 -49
- package/dist/wire/request.d.ts.map +1 -0
- package/dist/{wire-request.js → wire/request.js} +113 -83
- package/dist/wire/request.js.map +1 -0
- package/package.json +7 -7
- package/dist/adapter-entry.d.ts +0 -16
- package/dist/adapter-entry.d.ts.map +0 -1
- package/dist/adapter-entry.js +0 -14
- package/dist/adapter-entry.js.map +0 -1
- package/dist/attachment-download.d.ts.map +0 -1
- package/dist/attachment-download.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/changes.d.ts.map +0 -1
- package/dist/changes.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/did-entry.d.ts.map +0 -1
- package/dist/did-entry.js.map +0 -1
- package/dist/did.d.ts.map +0 -1
- package/dist/did.js.map +0 -1
- package/dist/identity-bindings.d.ts.map +0 -1
- package/dist/identity-bindings.js.map +0 -1
- package/dist/limits.d.ts.map +0 -1
- package/dist/limits.js.map +0 -1
- package/dist/record-id.d.ts +0 -70
- package/dist/record-id.d.ts.map +0 -1
- package/dist/record-id.js +0 -127
- package/dist/record-id.js.map +0 -1
- package/dist/scoped-stack.d.ts +0 -549
- package/dist/scoped-stack.d.ts.map +0 -1
- package/dist/scoped-stack.js +0 -1301
- package/dist/scoped-stack.js.map +0 -1
- package/dist/sort.d.ts.map +0 -1
- package/dist/sort.js.map +0 -1
- package/dist/stack-reads.d.ts +0 -38
- package/dist/stack-reads.d.ts.map +0 -1
- package/dist/stack-reads.js +0 -79
- package/dist/stack-reads.js.map +0 -1
- package/dist/stack.d.ts +0 -1032
- package/dist/stack.d.ts.map +0 -1
- package/dist/stack.js +0 -2337
- package/dist/stack.js.map +0 -1
- package/dist/types.d.ts +0 -1278
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -51
- package/dist/types.js.map +0 -1
- package/dist/wire-body.d.ts.map +0 -1
- package/dist/wire-body.js.map +0 -1
- package/dist/wire-entry.d.ts.map +0 -1
- package/dist/wire-entry.js.map +0 -1
- package/dist/wire-record.d.ts +0 -50
- package/dist/wire-record.d.ts.map +0 -1
- package/dist/wire-record.js.map +0 -1
- package/dist/wire-request.d.ts.map +0 -1
- package/dist/wire-request.js.map +0 -1
- /package/dist/{did.d.ts → did/did.d.ts} +0 -0
- /package/dist/{identity-bindings.d.ts → stack/identity-bindings.d.ts} +0 -0
package/dist/testing.d.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @haverstack/core/testing
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* In-memory adapters for tests: MemoryAdapter, which implements the whole
|
|
5
|
+
* StackAdapter contract — every filter, sort and capability a local
|
|
6
|
+
* adapter must declare — so code under test runs against real predicates;
|
|
7
|
+
* and IncapableMemoryAdapter, which declares none of the optional ones, for
|
|
8
|
+
* exercising the capability-gated fallbacks. Audience: tests in this repo
|
|
9
|
+
* and in apps built on it.
|
|
10
|
+
*/
|
|
11
|
+
import type { StackAdapter, StackRecord, StackType, TypeId, RecordVersion, RecordJournalEntry, JournalOptions, MutateOptions, JournalQuery, VersionsQuery, AspectWriteOptions, ElementWriteOptions, AdapterQuery, QueryResult, AssociationEdit, AuthorityAssociation, DataAssociation, StackCapabilities, BlobInfo, Reach } from './types/index.js';
|
|
2
12
|
export type MemoryAdapterOpenOptions = {
|
|
3
13
|
ownerEntityId: string;
|
|
4
14
|
timezone?: string;
|
|
5
15
|
};
|
|
6
16
|
/**
|
|
7
|
-
* In-memory StackAdapter with
|
|
17
|
+
* In-memory StackAdapter with keyed cursor pagination. Implements
|
|
8
18
|
* the full RecordFilter shape (mirroring sqlite-shared's recordConditions)
|
|
9
19
|
* so permission logic under test exercises real predicates. Declares
|
|
10
20
|
* `filter.content: 'path'`, as every local adapter must; tests needing
|
|
@@ -28,15 +38,15 @@ export declare class MemoryAdapter implements StackAdapter {
|
|
|
28
38
|
static open(opts: MemoryAdapterOpenOptions): Promise<MemoryAdapter>;
|
|
29
39
|
createRecord(record: StackRecord, opts?: JournalOptions): Promise<{
|
|
30
40
|
content: Record<string, unknown>;
|
|
31
|
-
id: import("./
|
|
41
|
+
id: import("./index.js").RecordId;
|
|
32
42
|
typeId: TypeId;
|
|
33
43
|
createdAt: Date;
|
|
34
44
|
updatedAt: Date;
|
|
35
45
|
version: number;
|
|
36
|
-
parentId?: import("./
|
|
37
|
-
appId?: import("./
|
|
38
|
-
createdBy?: import("./
|
|
39
|
-
updatedBy?: import("./
|
|
46
|
+
parentId?: import("./index.js").RecordId;
|
|
47
|
+
appId?: import("./index.js").AppId;
|
|
48
|
+
createdBy?: import("./index.js").Actor;
|
|
49
|
+
updatedBy?: import("./index.js").Actor;
|
|
40
50
|
deletedAt?: Date;
|
|
41
51
|
unlistedAt?: Date;
|
|
42
52
|
permissions?: AuthorityAssociation[];
|
|
@@ -59,27 +69,33 @@ export declare class MemoryAdapter implements StackAdapter {
|
|
|
59
69
|
* computed on the fly since MemoryAdapter has no persisted index.
|
|
60
70
|
*/
|
|
61
71
|
private hasFileRefTo;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
patchContent(id: string, patch: Record<string, unknown | null>, opts?: MutateOptions): Promise<StackRecord>;
|
|
73
|
+
/** The record a write addresses, past its `ifVersion` check. */
|
|
74
|
+
private requireForWrite;
|
|
75
|
+
/** requireForWrite() for an aspect write, which refuses a tombstone first. */
|
|
76
|
+
private requireLiveForWrite;
|
|
77
|
+
private store;
|
|
78
|
+
setParent(id: string, parentId: string | null, opts?: AspectWriteOptions): Promise<StackRecord>;
|
|
79
|
+
setUnlisted(id: string, unlisted: boolean, opts?: AspectWriteOptions): Promise<StackRecord>;
|
|
80
|
+
editAssociations(id: string, edits: AssociationEdit[], opts?: ElementWriteOptions<DataAssociation>): Promise<StackRecord>;
|
|
81
|
+
setAssociations(id: string, associations: DataAssociation[], opts?: ElementWriteOptions<DataAssociation>): Promise<StackRecord>;
|
|
82
|
+
editAccess(id: string, edits: AssociationEdit[], opts?: ElementWriteOptions<AuthorityAssociation>): Promise<StackRecord>;
|
|
83
|
+
setAccess(id: string, permissions: AuthorityAssociation[], opts?: ElementWriteOptions<AuthorityAssociation>): Promise<StackRecord>;
|
|
84
|
+
/** requireLiveForWrite(), past the `expectSet` check on the half it writes too. */
|
|
85
|
+
private requireSetForWrite;
|
|
67
86
|
deleteRecord(id: string, opts?: {
|
|
68
87
|
purge?: boolean;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
76
|
-
/** Cursor is a stringified offset into insertion order. */
|
|
77
|
-
queryRecords(query: StackQuery): Promise<QueryResult>;
|
|
88
|
+
} & MutateOptions): Promise<StackRecord | null>;
|
|
89
|
+
undeleteRecord(id: string, opts?: MutateOptions): Promise<StackRecord>;
|
|
90
|
+
/** The cursor names the last record a page held, by its place in the order. */
|
|
91
|
+
queryRecords(query: AdapterQuery): Promise<QueryResult>;
|
|
92
|
+
queryReadable(query: AdapterQuery, reach: Reach): Promise<QueryResult>;
|
|
93
|
+
private select;
|
|
78
94
|
/** The sort a cursor was minted under, as an opaque comparable descriptor. */
|
|
79
95
|
private sortDescriptor;
|
|
80
96
|
/**
|
|
81
|
-
*
|
|
82
|
-
* minted under — mirroring the real adapters'
|
|
97
|
+
* The last record's place in the order, paired with the sort it was
|
|
98
|
+
* minted under — mirroring the real adapters' keyed cursors.
|
|
83
99
|
* See docs/spec/data-model.md § Sorting and pagination.
|
|
84
100
|
*/
|
|
85
101
|
private encodeCursor;
|
|
@@ -98,6 +114,9 @@ export declare class MemoryAdapter implements StackAdapter {
|
|
|
98
114
|
* (docs/spec/data-model.md § Sorting by a content field).
|
|
99
115
|
*/
|
|
100
116
|
private sortRecords;
|
|
117
|
+
/** Where a record falls in the order `sort` names: its value there, then its id. */
|
|
118
|
+
private orderKey;
|
|
119
|
+
private compareKeys;
|
|
101
120
|
/**
|
|
102
121
|
* The record's ordered value at `field`, or null when it holds none.
|
|
103
122
|
* Read through the declared schema — a field only orders as the kind
|
|
@@ -105,8 +124,6 @@ export declare class MemoryAdapter implements StackAdapter {
|
|
|
105
124
|
* nothing at all.
|
|
106
125
|
*/
|
|
107
126
|
private sortEntry;
|
|
108
|
-
/** Never bumps `version`/`updatedAt` — see StackRecordAdapter.amendAssociations(). */
|
|
109
|
-
amendAssociations(id: string, changes: AssociationEdit[], opts?: JournalOptions): Promise<StackRecord>;
|
|
110
127
|
getVersions(id: string, query?: VersionsQuery): Promise<RecordVersion[]>;
|
|
111
128
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
112
129
|
/**
|
|
@@ -129,14 +146,8 @@ export declare class MemoryAdapter implements StackAdapter {
|
|
|
129
146
|
* which is overwritten so the interrupted write can finally complete.
|
|
130
147
|
*/
|
|
131
148
|
private snapshotBeforeMutation;
|
|
132
|
-
restoreVersion(id: string, version: number, opts?:
|
|
133
|
-
|
|
134
|
-
snapshot?: RecordVersion;
|
|
135
|
-
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
136
|
-
commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
|
|
137
|
-
ifVersion?: number;
|
|
138
|
-
snapshot?: RecordVersion;
|
|
139
|
-
} & ActorOptions & JournalOptions): Promise<StackRecord>;
|
|
149
|
+
restoreVersion(id: string, version: number, opts?: MutateOptions): Promise<StackRecord>;
|
|
150
|
+
commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: MutateOptions): Promise<StackRecord>;
|
|
140
151
|
/**
|
|
141
152
|
* Append one entry, stamping the record-derived half from the row as it
|
|
142
153
|
* now stands. `seq` is allocated here rather than by the caller, which
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,SAAS,EACT,MAAM,EACN,aAAa,EACb,kBAAkB,EAElB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -1,51 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @haverstack/core/testing
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* In-memory adapters for tests: MemoryAdapter, which implements the whole
|
|
5
|
+
* StackAdapter contract — every filter, sort and capability a local
|
|
6
|
+
* adapter must declare — so code under test runs against real predicates;
|
|
7
|
+
* and IncapableMemoryAdapter, which declares none of the optional ones, for
|
|
8
|
+
* exercising the capability-gated fallbacks. Audience: tests in this repo
|
|
9
|
+
* and in apps built on it.
|
|
10
|
+
*/
|
|
11
|
+
import { SYSTEM_TYPES } from './types/index.js';
|
|
2
12
|
import { applyMergePatch } from './merge.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (sort.direction === undefined) {
|
|
10
|
-
throw new StackBadRequestError('queryRecords() received a sort without a direction.');
|
|
11
|
-
}
|
|
12
|
-
return sort.direction;
|
|
13
|
-
};
|
|
14
|
-
/** An array stands for its elements; anything else stands for itself. */
|
|
15
|
-
const spreadValue = (value) => (Array.isArray(value) ? value : [value]);
|
|
13
|
+
import { baseIdOf, fileRefFields } from './schema.js';
|
|
14
|
+
import { compareSortEntries, constantSortEntry, contentSortEntry } from './adapter/sort.js';
|
|
15
|
+
import { inReach } from './adapter/reach.js';
|
|
16
|
+
import { ElementSetMovedError, softDeletedRefusal, StackVersionConflictError, StackConflictError, StackNotFoundError, StackBadRequestError, } from './errors.js';
|
|
17
|
+
import { matchesContent } from './content-match.js';
|
|
18
|
+
import { applyAssociationEdits, sameAssociationSet } from './associations/identity.js';
|
|
16
19
|
/** A FileId is the SHA-256 hex digest of its bytes — see docs/spec/attachments.md. */
|
|
17
20
|
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
18
|
-
/**
|
|
19
|
-
* Every value reachable by walking `segments`, spreading arrays
|
|
20
|
-
* element-wise at each step — the same walk sqlite-shared's
|
|
21
|
-
* contentPathExists() performs in SQL, and the reason a filter answers the
|
|
22
|
-
* same question against either. An empty result means the path reaches
|
|
23
|
-
* nothing, which is what a `null` filter value matches.
|
|
24
|
-
* See docs/spec/data-model.md § Nested content paths.
|
|
25
|
-
*/
|
|
26
|
-
const valuesAtContentPath = (content, segments) => {
|
|
27
|
-
let current = [content];
|
|
28
|
-
for (const segment of segments) {
|
|
29
|
-
const next = [];
|
|
30
|
-
for (const value of current) {
|
|
31
|
-
for (const item of spreadValue(value)) {
|
|
32
|
-
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
33
|
-
continue;
|
|
34
|
-
if (Object.hasOwn(item, segment))
|
|
35
|
-
next.push(item[segment]);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
current = next;
|
|
39
|
-
}
|
|
40
|
-
// The leaf spreads too, so a filter on `tags` matches a record whose
|
|
41
|
-
// `tags` array contains the value.
|
|
42
|
-
return current.flatMap(spreadValue);
|
|
43
|
-
};
|
|
44
21
|
// An `undefined` field is a field the record does not have; storing the key
|
|
45
22
|
// would make `'u' in content` differ from adapters that serialize to JSON.
|
|
46
23
|
const withoutUndefined = (content) => Object.fromEntries(Object.entries(content).filter(([, v]) => v !== undefined));
|
|
47
24
|
/**
|
|
48
|
-
* In-memory StackAdapter with
|
|
25
|
+
* In-memory StackAdapter with keyed cursor pagination. Implements
|
|
49
26
|
* the full RecordFilter shape (mirroring sqlite-shared's recordConditions)
|
|
50
27
|
* so permission logic under test exercises real predicates. Declares
|
|
51
28
|
* `filter.content: 'path'`, as every local adapter must; tests needing
|
|
@@ -56,11 +33,13 @@ export class MemoryAdapter {
|
|
|
56
33
|
filter: {
|
|
57
34
|
content: 'path',
|
|
58
35
|
contentPresent: true,
|
|
36
|
+
contentByType: true,
|
|
59
37
|
search: false,
|
|
60
38
|
},
|
|
61
39
|
sort: {
|
|
62
40
|
fields: ['createdAt', 'updatedAt', 'version'],
|
|
63
41
|
contentField: true,
|
|
42
|
+
contentFieldByType: true,
|
|
64
43
|
},
|
|
65
44
|
limits: {
|
|
66
45
|
attachmentBytes: null,
|
|
@@ -129,43 +108,71 @@ export class MemoryAdapter {
|
|
|
129
108
|
if (!schema)
|
|
130
109
|
return false;
|
|
131
110
|
const content = record.content;
|
|
132
|
-
return
|
|
111
|
+
return fileRefFields(schema).some((field) => content[field] === fileId);
|
|
133
112
|
}
|
|
134
|
-
async
|
|
113
|
+
async patchContent(id, patch, opts = {}) {
|
|
114
|
+
const existing = this.requireForWrite(id, opts.ifVersion);
|
|
115
|
+
if (opts.snapshot)
|
|
116
|
+
this.snapshotBeforeMutation(id, opts.snapshot);
|
|
117
|
+
const content = withoutUndefined(applyMergePatch(existing.content, patch));
|
|
118
|
+
return this.store(id, this.bump({ ...existing, content }, opts), opts.journal);
|
|
119
|
+
}
|
|
120
|
+
/** The record a write addresses, past its `ifVersion` check. */
|
|
121
|
+
requireForWrite(id, ifVersion) {
|
|
135
122
|
const existing = this.records.get(id);
|
|
136
123
|
if (!existing)
|
|
137
124
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
138
|
-
this.checkExpectedVersion(existing,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
this.checkExpectedVersion(existing, ifVersion);
|
|
126
|
+
return existing;
|
|
127
|
+
}
|
|
128
|
+
/** requireForWrite() for an aspect write, which refuses a tombstone first. */
|
|
129
|
+
requireLiveForWrite(id, ifVersion) {
|
|
130
|
+
const existing = this.records.get(id);
|
|
131
|
+
if (existing?.deletedAt)
|
|
132
|
+
throw softDeletedRefusal(id);
|
|
133
|
+
return this.requireForWrite(id, ifVersion);
|
|
134
|
+
}
|
|
135
|
+
store(id, record, journal) {
|
|
136
|
+
this.records.set(id, record);
|
|
137
|
+
this.appendJournal(id, journal, record);
|
|
138
|
+
return record;
|
|
139
|
+
}
|
|
140
|
+
// An optional native field is destructured out and re-added only when
|
|
141
|
+
// set: assigning `undefined` would leave the key present, which is how a
|
|
142
|
+
// record grows a `parentId: undefined` every `in` check reads as a container.
|
|
143
|
+
async setParent(id, parentId, opts = {}) {
|
|
144
|
+
const { parentId: _p, ...rest } = this.requireLiveForWrite(id, opts.ifVersion);
|
|
145
|
+
return this.store(id, { ...rest, ...(parentId !== null && { parentId }) }, opts.journal);
|
|
146
|
+
}
|
|
147
|
+
async setUnlisted(id, unlisted, opts = {}) {
|
|
148
|
+
const { unlistedAt: _u, ...rest } = this.requireLiveForWrite(id, opts.ifVersion);
|
|
149
|
+
return this.store(id, { ...rest, ...(unlisted && { unlistedAt: new Date() }) }, opts.journal);
|
|
150
|
+
}
|
|
151
|
+
async editAssociations(id, edits, opts = {}) {
|
|
152
|
+
const existing = this.requireSetForWrite(id, 'associations', opts);
|
|
153
|
+
const next = applyAssociationEdits(existing.associations ?? [], edits);
|
|
154
|
+
return this.store(id, withHalf(existing, 'associations', next), opts.journal);
|
|
155
|
+
}
|
|
156
|
+
async setAssociations(id, associations, opts = {}) {
|
|
157
|
+
const existing = this.requireSetForWrite(id, 'associations', opts);
|
|
158
|
+
return this.store(id, withHalf(existing, 'associations', associations), opts.journal);
|
|
159
|
+
}
|
|
160
|
+
async editAccess(id, edits, opts = {}) {
|
|
161
|
+
const existing = this.requireSetForWrite(id, 'permissions', opts);
|
|
162
|
+
const next = applyAssociationEdits(existing.permissions ?? [], edits);
|
|
163
|
+
return this.store(id, withHalf(existing, 'permissions', next), opts.journal);
|
|
164
|
+
}
|
|
165
|
+
async setAccess(id, permissions, opts = {}) {
|
|
166
|
+
const existing = this.requireSetForWrite(id, 'permissions', opts);
|
|
167
|
+
return this.store(id, withHalf(existing, 'permissions', permissions), opts.journal);
|
|
168
|
+
}
|
|
169
|
+
/** requireLiveForWrite(), past the `expectSet` check on the half it writes too. */
|
|
170
|
+
requireSetForWrite(id, half, opts) {
|
|
171
|
+
const existing = this.requireLiveForWrite(id, opts.ifVersion);
|
|
172
|
+
if (opts.expectSet && !sameAssociationSet(existing[half] ?? [], opts.expectSet)) {
|
|
173
|
+
throw new ElementSetMovedError(id);
|
|
149
174
|
}
|
|
150
|
-
|
|
151
|
-
// is what keeps a tag write from touching an ACL it was never shown.
|
|
152
|
-
if (changes.permissions)
|
|
153
|
-
next = withPermissions(next, changes.permissions);
|
|
154
|
-
if (changes.associations)
|
|
155
|
-
next = withAssociations(next, changes.associations);
|
|
156
|
-
const parentId = changes.parentId !== undefined ? changes.parentId : (existing.parentId ?? null);
|
|
157
|
-
if (parentId !== null)
|
|
158
|
-
next = { ...next, parentId };
|
|
159
|
-
const unlisted = changes.unlisted !== undefined ? changes.unlisted : Boolean(existing.unlistedAt);
|
|
160
|
-
if (unlisted)
|
|
161
|
-
next = { ...next, unlistedAt: existing.unlistedAt ?? new Date() };
|
|
162
|
-
// A change set touching only `associations` doesn't bump — see
|
|
163
|
-
// docs/spec/versioning.md § Version history. `next` already carries
|
|
164
|
-
// the prior version/updatedAt/updatedBy untouched.
|
|
165
|
-
const updated = opts.bumpsVersion === false ? next : this.bump(next, opts);
|
|
166
|
-
this.records.set(id, updated);
|
|
167
|
-
this.appendJournal(id, opts.journal, updated);
|
|
168
|
-
return updated;
|
|
175
|
+
return existing;
|
|
169
176
|
}
|
|
170
177
|
async deleteRecord(id, opts = {}) {
|
|
171
178
|
const record = this.records.get(id);
|
|
@@ -209,10 +216,18 @@ export class MemoryAdapter {
|
|
|
209
216
|
this.appendJournal(id, opts.journal, updated);
|
|
210
217
|
return updated;
|
|
211
218
|
}
|
|
212
|
-
/**
|
|
219
|
+
/** The cursor names the last record a page held, by its place in the order. */
|
|
213
220
|
async queryRecords(query) {
|
|
221
|
+
return this.select(query);
|
|
222
|
+
}
|
|
223
|
+
async queryReadable(query, reach) {
|
|
224
|
+
return this.select(query, reach);
|
|
225
|
+
}
|
|
226
|
+
select(query, reach) {
|
|
214
227
|
const f = query.filter ?? {};
|
|
215
228
|
let results = this.order.map((id) => this.records.get(id));
|
|
229
|
+
if (reach)
|
|
230
|
+
results = results.filter((r) => inReach(r, reach));
|
|
216
231
|
// _config is addressable only by ID (getRecord), never returned by a
|
|
217
232
|
// generic query — mirroring the real SQL adapters' WHERE exclusion.
|
|
218
233
|
results = results.filter((r) => r.id !== SYSTEM_TYPES.CONFIG);
|
|
@@ -224,6 +239,10 @@ export class MemoryAdapter {
|
|
|
224
239
|
const ids = Array.isArray(f.typeId) ? f.typeId : [f.typeId];
|
|
225
240
|
results = results.filter((r) => ids.includes(r.typeId));
|
|
226
241
|
}
|
|
242
|
+
if (f.baseId) {
|
|
243
|
+
const families = Array.isArray(f.baseId) ? f.baseId : [f.baseId];
|
|
244
|
+
results = results.filter((r) => families.includes(baseIdOf(r.typeId)));
|
|
245
|
+
}
|
|
227
246
|
if (f.parentId !== undefined) {
|
|
228
247
|
results =
|
|
229
248
|
f.parentId === null
|
|
@@ -271,48 +290,43 @@ export class MemoryAdapter {
|
|
|
271
290
|
const relatedTo = f.relatedTo;
|
|
272
291
|
results = results.filter((r) => matchesRelatedTo(r.associations, relatedTo));
|
|
273
292
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// satisfying a null filter (docs/spec/data-model.md § Filter).
|
|
277
|
-
if (f.content) {
|
|
278
|
-
const entries = Object.entries(f.content).map(([key, value]) => [parseContentFilterKey(key), value]);
|
|
279
|
-
results = results.filter((r) => entries.every(([segments, value]) => {
|
|
280
|
-
const found = valuesAtContentPath(r.content, segments);
|
|
281
|
-
if (value === null || value === undefined) {
|
|
282
|
-
return found.length === 0 || found.some((v) => v === null || v === undefined);
|
|
283
|
-
}
|
|
284
|
-
return found.some((v) => v === value);
|
|
285
|
-
}));
|
|
293
|
+
if (f.content || f.contentPresent?.length) {
|
|
294
|
+
results = results.filter((r) => matchesContent(r.content, f));
|
|
286
295
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const paths = f.contentPresent.map((key) => parseContentFilterKey(key));
|
|
292
|
-
results = results.filter((r) => paths.every((segments) => valuesAtContentPath(r.content, segments).some((v) => v !== null && v !== undefined)));
|
|
296
|
+
if (f.contentByType) {
|
|
297
|
+
const arms = f.contentByType;
|
|
298
|
+
results = results.filter((r) => Object.hasOwn(arms, r.typeId) &&
|
|
299
|
+
matchesContent(r.content, arms[r.typeId]));
|
|
293
300
|
}
|
|
294
|
-
|
|
295
|
-
const
|
|
301
|
+
results = this.sortRecords(results, query.sort);
|
|
302
|
+
const { limit } = query;
|
|
303
|
+
// Resume after the record the cursor names, wherever it now falls: a
|
|
304
|
+
// change between pages moves no boundary, as with the real adapters'
|
|
305
|
+
// keyed cursors.
|
|
306
|
+
const after = query.cursor ? this.decodeCursor(query.cursor, query.sort) : null;
|
|
307
|
+
const resume = after
|
|
308
|
+
? results.findIndex((r) => this.compareKeys(this.orderKey(r, query.sort), after, query.sort) > 0)
|
|
309
|
+
: 0;
|
|
310
|
+
const start = resume === -1 ? results.length : resume;
|
|
296
311
|
const page = results.slice(start, start + limit);
|
|
297
|
-
const
|
|
298
|
-
const cursor =
|
|
312
|
+
const last = page[page.length - 1];
|
|
313
|
+
const cursor = last && start + limit < results.length
|
|
314
|
+
? this.encodeCursor(query.sort, this.orderKey(last, query.sort))
|
|
315
|
+
: null;
|
|
299
316
|
return { records: page, cursor };
|
|
300
317
|
}
|
|
301
318
|
/** The sort a cursor was minted under, as an opaque comparable descriptor. */
|
|
302
319
|
sortDescriptor(sort) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
sort?.contentField === undefined ? (sort?.field ?? 'createdAt') : null,
|
|
306
|
-
sort ? requireDirection(sort) : 'desc',
|
|
307
|
-
]);
|
|
320
|
+
const content = sort.contentField ?? sort.contentFieldByType;
|
|
321
|
+
return JSON.stringify([content ?? null, sort.field ?? null, sort.direction]);
|
|
308
322
|
}
|
|
309
323
|
/**
|
|
310
|
-
*
|
|
311
|
-
* minted under — mirroring the real adapters'
|
|
324
|
+
* The last record's place in the order, paired with the sort it was
|
|
325
|
+
* minted under — mirroring the real adapters' keyed cursors.
|
|
312
326
|
* See docs/spec/data-model.md § Sorting and pagination.
|
|
313
327
|
*/
|
|
314
|
-
encodeCursor(sort,
|
|
315
|
-
const json = JSON.stringify({ d: this.sortDescriptor(sort),
|
|
328
|
+
encodeCursor(sort, key) {
|
|
329
|
+
const json = JSON.stringify({ d: this.sortDescriptor(sort), k: key });
|
|
316
330
|
// Explicit UTF-8 step before btoa, matching sqlite-shared's codec: the
|
|
317
331
|
// descriptor embeds `sort.contentField` verbatim, and a non-Latin-1
|
|
318
332
|
// field name would otherwise make btoa throw an InvalidCharacterError
|
|
@@ -341,19 +355,19 @@ export class MemoryAdapter {
|
|
|
341
355
|
catch {
|
|
342
356
|
throw new StackBadRequestError(`Malformed cursor: "${cursor}"`);
|
|
343
357
|
}
|
|
344
|
-
const { d,
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
// the wire uninspected (wire-request copies `?cursor=` verbatim), so
|
|
349
|
-
// this is the only place the offset is checked at all.
|
|
350
|
-
if (typeof d !== 'string' || typeof o !== 'number' || !Number.isInteger(o) || o < 0) {
|
|
358
|
+
const { d, k } = (parsed ?? {});
|
|
359
|
+
// Cursors arrive from the wire uninspected (wire/request.ts copies
|
|
360
|
+
// `?cursor=` verbatim), so this is the only place the key is checked.
|
|
361
|
+
if (typeof d !== 'string')
|
|
351
362
|
throw new StackBadRequestError(`Malformed cursor: "${cursor}"`);
|
|
352
|
-
|
|
363
|
+
// The sort first: a key is only well-formed or not for the order it names.
|
|
353
364
|
if (d !== this.sortDescriptor(sort)) {
|
|
354
365
|
throw new StackBadRequestError(`Cursor was minted under a different sort than this query names.`);
|
|
355
366
|
}
|
|
356
|
-
|
|
367
|
+
const native = sort?.contentField === undefined && sort?.contentFieldByType === undefined;
|
|
368
|
+
if (!isOrderKey(k, native))
|
|
369
|
+
throw new StackBadRequestError(`Malformed cursor: "${cursor}"`);
|
|
370
|
+
return k;
|
|
357
371
|
}
|
|
358
372
|
/**
|
|
359
373
|
* Order a result set the way a materialized sort index does: absent
|
|
@@ -363,19 +377,27 @@ export class MemoryAdapter {
|
|
|
363
377
|
* (docs/spec/data-model.md § Sorting by a content field).
|
|
364
378
|
*/
|
|
365
379
|
sortRecords(records, sort) {
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
380
|
+
const keys = new Map(records.map((r) => [r.id, this.orderKey(r, sort)]));
|
|
381
|
+
return [...records].sort((a, b) => this.compareKeys(keys.get(a.id), keys.get(b.id), sort));
|
|
382
|
+
}
|
|
383
|
+
/** Where a record falls in the order `sort` names: its value there, then its id. */
|
|
384
|
+
orderKey(record, sort) {
|
|
385
|
+
const { field } = sort;
|
|
386
|
+
if (field !== undefined) {
|
|
387
|
+
const value = field === 'version'
|
|
388
|
+
? record.version
|
|
389
|
+
: (field === 'updatedAt' ? record.updatedAt : record.createdAt).getTime();
|
|
390
|
+
return { value, id: record.id };
|
|
375
391
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
392
|
+
return { value: this.sortEntry(record, sort), id: record.id };
|
|
393
|
+
}
|
|
394
|
+
compareKeys(a, b, sort) {
|
|
395
|
+
const { direction } = sort;
|
|
396
|
+
const sign = direction === 'asc' ? 1 : -1;
|
|
397
|
+
const byValue = typeof a.value === 'number' && typeof b.value === 'number'
|
|
398
|
+
? sign * (a.value - b.value)
|
|
399
|
+
: compareSortEntries(a.value, b.value, direction);
|
|
400
|
+
return byValue || (a.id < b.id ? -sign : a.id > b.id ? sign : 0);
|
|
379
401
|
}
|
|
380
402
|
/**
|
|
381
403
|
* The record's ordered value at `field`, or null when it holds none.
|
|
@@ -383,23 +405,23 @@ export class MemoryAdapter {
|
|
|
383
405
|
* its type says it is, and an undeclared or non-scalar field orders as
|
|
384
406
|
* nothing at all.
|
|
385
407
|
*/
|
|
386
|
-
sortEntry(record,
|
|
408
|
+
sortEntry(record, sort) {
|
|
409
|
+
let field = sort.contentField;
|
|
410
|
+
if (sort.contentFieldByType) {
|
|
411
|
+
const key = Object.hasOwn(sort.contentFieldByType, record.typeId)
|
|
412
|
+
? sort.contentFieldByType[record.typeId]
|
|
413
|
+
: { value: null };
|
|
414
|
+
if (key.field === undefined)
|
|
415
|
+
return constantSortEntry(key.value ?? null);
|
|
416
|
+
field = key.field;
|
|
417
|
+
}
|
|
418
|
+
if (field === undefined)
|
|
419
|
+
return null;
|
|
387
420
|
const def = this.types.get(record.typeId)?.schema[field];
|
|
388
421
|
if (!def || def.kind === 'array' || def.kind === 'object')
|
|
389
422
|
return null;
|
|
390
423
|
return contentSortEntry(def.kind, record.content[field]);
|
|
391
424
|
}
|
|
392
|
-
/** Never bumps `version`/`updatedAt` — see StackRecordAdapter.amendAssociations(). */
|
|
393
|
-
async amendAssociations(id, changes, opts = {}) {
|
|
394
|
-
assertOneSurface(changes);
|
|
395
|
-
const record = this.records.get(id);
|
|
396
|
-
if (!record)
|
|
397
|
-
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
398
|
-
const updated = withAssociationSet(record, applyAssociationEdits(allAssociations(record), changes));
|
|
399
|
-
this.records.set(id, updated);
|
|
400
|
-
this.appendJournal(id, opts.journal, updated);
|
|
401
|
-
return updated;
|
|
402
|
-
}
|
|
403
425
|
async getVersions(id, query = {}) {
|
|
404
426
|
const newestFirst = [...(this.versions.get(id) ?? [])].sort((a, b) => b.version - a.version);
|
|
405
427
|
const before = query.beforeVersion === undefined
|
|
@@ -569,11 +591,13 @@ export class IncapableMemoryAdapter extends MemoryAdapter {
|
|
|
569
591
|
filter: {
|
|
570
592
|
content: 'none',
|
|
571
593
|
contentPresent: false,
|
|
594
|
+
contentByType: false,
|
|
572
595
|
search: false,
|
|
573
596
|
},
|
|
574
597
|
sort: {
|
|
575
598
|
fields: ['createdAt', 'updatedAt', 'version'],
|
|
576
599
|
contentField: false,
|
|
600
|
+
contentFieldByType: false,
|
|
577
601
|
},
|
|
578
602
|
limits: {
|
|
579
603
|
attachmentBytes: null,
|
|
@@ -584,45 +608,25 @@ export class IncapableMemoryAdapter extends MemoryAdapter {
|
|
|
584
608
|
return new IncapableMemoryAdapter(opts);
|
|
585
609
|
}
|
|
586
610
|
}
|
|
587
|
-
/** Whether
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
* cannot represent. Both projections are written, omitting either key when
|
|
602
|
-
* empty — mirroring the SQL adapters' rowToRecord, so the same mutation
|
|
603
|
-
* sequence produces identically-shaped records on the test double and real
|
|
604
|
-
* storage. See docs/spec/adapters.md § Associations are keyed by identity.
|
|
605
|
-
*/
|
|
606
|
-
function withAssociationSet(record, associations) {
|
|
607
|
-
const keyed = associations.reduce((acc, a) => acc.some((b) => associationEqual(a, b))
|
|
608
|
-
? acc.map((b) => (associationEqual(a, b) ? a : b))
|
|
609
|
-
: [...acc, a], []);
|
|
610
|
-
const { associations: _dropA, permissions: _dropP, ...rest } = record;
|
|
611
|
-
const data = keyed.filter((a) => !isAuthority(a));
|
|
612
|
-
const authority = keyed.filter(isAuthority);
|
|
613
|
-
return {
|
|
614
|
-
...rest,
|
|
615
|
-
...(data.length && { associations: data }),
|
|
616
|
-
...(authority.length && { permissions: authority }),
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
/** withAssociationSet() over the data half alone — the `associations` key. */
|
|
620
|
-
function withAssociations(record, associations) {
|
|
621
|
-
return withAssociationSet(record, [...associations, ...(record.permissions ?? [])]);
|
|
611
|
+
/** Whether `k` is a key of the order a native (`native`) or content sort names. */
|
|
612
|
+
function isOrderKey(k, native) {
|
|
613
|
+
if (!k || typeof k !== 'object')
|
|
614
|
+
return false;
|
|
615
|
+
const { value, id } = k;
|
|
616
|
+
if (typeof id !== 'string')
|
|
617
|
+
return false;
|
|
618
|
+
if (native)
|
|
619
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
620
|
+
if (value === null)
|
|
621
|
+
return true;
|
|
622
|
+
const e = value;
|
|
623
|
+
return ((e?.kind === 'num' && typeof e.num === 'number') ||
|
|
624
|
+
(e?.kind === 'text' && typeof e.text === 'string' && typeof e.key === 'string'));
|
|
622
625
|
}
|
|
623
|
-
/**
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
+
/** One aspect's set replaced; an empty set is omitted, as the SQL adapters' rowToRecord does. */
|
|
627
|
+
function withHalf(record, half, elements) {
|
|
628
|
+
const { [half]: _dropped, ...rest } = record;
|
|
629
|
+
return { ...rest, ...(elements.length && { [half]: elements }) };
|
|
626
630
|
}
|
|
627
631
|
/**
|
|
628
632
|
* Mirrors sqlite-shared's relatedTo predicate: a bare label matches every
|