@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.
Files changed (317) hide show
  1. package/README.md +124 -64
  2. package/dist/access.d.ts +43 -47
  3. package/dist/access.d.ts.map +1 -1
  4. package/dist/access.js +66 -101
  5. package/dist/access.js.map +1 -1
  6. package/dist/{combine.d.ts → adapter/combine.d.ts} +8 -1
  7. package/dist/adapter/combine.d.ts.map +1 -0
  8. package/dist/{combine.js → adapter/combine.js} +18 -3
  9. package/dist/adapter/combine.js.map +1 -0
  10. package/dist/adapter/index.d.ts +19 -0
  11. package/dist/adapter/index.d.ts.map +1 -0
  12. package/dist/adapter/index.js +16 -0
  13. package/dist/adapter/index.js.map +1 -0
  14. package/dist/adapter/reach.d.ts +14 -0
  15. package/dist/adapter/reach.d.ts.map +1 -0
  16. package/dist/adapter/reach.js +31 -0
  17. package/dist/adapter/reach.js.map +1 -0
  18. package/dist/{sort.d.ts → adapter/sort.d.ts} +20 -20
  19. package/dist/adapter/sort.d.ts.map +1 -0
  20. package/dist/{sort.js → adapter/sort.js} +37 -27
  21. package/dist/adapter/sort.js.map +1 -0
  22. package/dist/associations/identity.d.ts +69 -0
  23. package/dist/associations/identity.d.ts.map +1 -0
  24. package/dist/associations/identity.js +174 -0
  25. package/dist/associations/identity.js.map +1 -0
  26. package/dist/associations/validation.d.ts +83 -0
  27. package/dist/associations/validation.d.ts.map +1 -0
  28. package/dist/associations/validation.js +312 -0
  29. package/dist/associations/validation.js.map +1 -0
  30. package/dist/attachment-records.d.ts +48 -0
  31. package/dist/attachment-records.d.ts.map +1 -0
  32. package/dist/attachment-records.js +52 -0
  33. package/dist/attachment-records.js.map +1 -0
  34. package/dist/collection.d.ts +178 -0
  35. package/dist/collection.d.ts.map +1 -0
  36. package/dist/collection.js +397 -0
  37. package/dist/collection.js.map +1 -0
  38. package/dist/content-match.d.ts +25 -0
  39. package/dist/content-match.d.ts.map +1 -0
  40. package/dist/content-match.js +51 -0
  41. package/dist/content-match.js.map +1 -0
  42. package/dist/content-translation.d.ts +42 -0
  43. package/dist/content-translation.d.ts.map +1 -0
  44. package/dist/content-translation.js +108 -0
  45. package/dist/content-translation.js.map +1 -0
  46. package/dist/did/did.d.ts.map +1 -0
  47. package/dist/{did.js → did/did.js} +3 -4
  48. package/dist/did/did.js.map +1 -0
  49. package/dist/{did-entry.d.ts → did/index.d.ts} +1 -1
  50. package/dist/did/index.d.ts.map +1 -0
  51. package/dist/{did-entry.js → did/index.js} +3 -3
  52. package/dist/did/index.js.map +1 -0
  53. package/dist/errors.d.ts +54 -66
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +58 -62
  56. package/dist/errors.js.map +1 -1
  57. package/dist/grants.d.ts +20 -44
  58. package/dist/grants.d.ts.map +1 -1
  59. package/dist/grants.js +69 -151
  60. package/dist/grants.js.map +1 -1
  61. package/dist/id.d.ts +13 -31
  62. package/dist/id.d.ts.map +1 -1
  63. package/dist/id.js +13 -31
  64. package/dist/id.js.map +1 -1
  65. package/dist/index.d.ts +13 -9
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -6
  68. package/dist/index.js.map +1 -1
  69. package/dist/install.d.ts +3 -6
  70. package/dist/install.d.ts.map +1 -1
  71. package/dist/install.js +6 -10
  72. package/dist/install.js.map +1 -1
  73. package/dist/lazy.d.ts +13 -0
  74. package/dist/lazy.d.ts.map +1 -0
  75. package/dist/lazy.js +27 -0
  76. package/dist/lazy.js.map +1 -0
  77. package/dist/merge.d.ts +1 -1
  78. package/dist/merge.d.ts.map +1 -1
  79. package/dist/merge.js +6 -12
  80. package/dist/merge.js.map +1 -1
  81. package/dist/migration-steps.d.ts +102 -0
  82. package/dist/migration-steps.d.ts.map +1 -0
  83. package/dist/migration-steps.js +293 -0
  84. package/dist/migration-steps.js.map +1 -0
  85. package/dist/query-validation.d.ts +68 -121
  86. package/dist/query-validation.d.ts.map +1 -1
  87. package/dist/query-validation.js +214 -380
  88. package/dist/query-validation.js.map +1 -1
  89. package/dist/record-changes.d.ts +11 -150
  90. package/dist/record-changes.d.ts.map +1 -1
  91. package/dist/record-changes.js +14 -305
  92. package/dist/record-changes.js.map +1 -1
  93. package/dist/schema.d.ts +18 -7
  94. package/dist/schema.d.ts.map +1 -1
  95. package/dist/schema.js +59 -30
  96. package/dist/schema.js.map +1 -1
  97. package/dist/scoped-stack/authority-snapshot.d.ts +37 -0
  98. package/dist/scoped-stack/authority-snapshot.d.ts.map +1 -0
  99. package/dist/scoped-stack/authority-snapshot.js +49 -0
  100. package/dist/scoped-stack/authority-snapshot.js.map +1 -0
  101. package/dist/scoped-stack/authority.d.ts +169 -0
  102. package/dist/scoped-stack/authority.d.ts.map +1 -0
  103. package/dist/scoped-stack/authority.js +408 -0
  104. package/dist/scoped-stack/authority.js.map +1 -0
  105. package/dist/scoped-stack/family-policy.d.ts +59 -0
  106. package/dist/scoped-stack/family-policy.d.ts.map +1 -0
  107. package/dist/scoped-stack/family-policy.js +68 -0
  108. package/dist/scoped-stack/family-policy.js.map +1 -0
  109. package/dist/scoped-stack/scoped-stack.d.ts +318 -0
  110. package/dist/scoped-stack/scoped-stack.d.ts.map +1 -0
  111. package/dist/scoped-stack/scoped-stack.js +695 -0
  112. package/dist/scoped-stack/scoped-stack.js.map +1 -0
  113. package/dist/scoped-stack/subscription.d.ts +36 -0
  114. package/dist/scoped-stack/subscription.d.ts.map +1 -0
  115. package/dist/scoped-stack/subscription.js +69 -0
  116. package/dist/scoped-stack/subscription.js.map +1 -0
  117. package/dist/stack/arguments.d.ts +27 -0
  118. package/dist/stack/arguments.d.ts.map +1 -0
  119. package/dist/stack/arguments.js +53 -0
  120. package/dist/stack/arguments.js.map +1 -0
  121. package/dist/stack/attachments.d.ts +24 -0
  122. package/dist/stack/attachments.d.ts.map +1 -0
  123. package/dist/stack/attachments.js +113 -0
  124. package/dist/stack/attachments.js.map +1 -0
  125. package/dist/{changes.d.ts → stack/changes.d.ts} +30 -72
  126. package/dist/stack/changes.d.ts.map +1 -0
  127. package/dist/{changes.js → stack/changes.js} +40 -74
  128. package/dist/stack/changes.js.map +1 -0
  129. package/dist/stack/client.d.ts +247 -0
  130. package/dist/stack/client.d.ts.map +1 -0
  131. package/dist/stack/client.js +9 -0
  132. package/dist/stack/client.js.map +1 -0
  133. package/dist/stack/identity-bindings.d.ts.map +1 -0
  134. package/dist/{identity-bindings.js → stack/identity-bindings.js} +5 -14
  135. package/dist/stack/identity-bindings.js.map +1 -0
  136. package/dist/stack/install-app.d.ts +22 -0
  137. package/dist/stack/install-app.d.ts.map +1 -0
  138. package/dist/stack/install-app.js +262 -0
  139. package/dist/stack/install-app.js.map +1 -0
  140. package/dist/stack/integrity.d.ts +94 -0
  141. package/dist/stack/integrity.d.ts.map +1 -0
  142. package/dist/stack/integrity.js +268 -0
  143. package/dist/stack/integrity.js.map +1 -0
  144. package/dist/{limits.d.ts → stack/limits.d.ts} +2 -5
  145. package/dist/stack/limits.d.ts.map +1 -0
  146. package/dist/{limits.js → stack/limits.js} +3 -6
  147. package/dist/stack/limits.js.map +1 -0
  148. package/dist/stack/migrations.d.ts +59 -0
  149. package/dist/stack/migrations.d.ts.map +1 -0
  150. package/dist/stack/migrations.js +141 -0
  151. package/dist/stack/migrations.js.map +1 -0
  152. package/dist/stack/reads.d.ts +69 -0
  153. package/dist/stack/reads.d.ts.map +1 -0
  154. package/dist/stack/reads.js +141 -0
  155. package/dist/stack/reads.js.map +1 -0
  156. package/dist/stack/record-id.d.ts +35 -0
  157. package/dist/stack/record-id.d.ts.map +1 -0
  158. package/dist/stack/record-id.js +82 -0
  159. package/dist/stack/record-id.js.map +1 -0
  160. package/dist/stack/stack.d.ts +444 -0
  161. package/dist/stack/stack.d.ts.map +1 -0
  162. package/dist/stack/stack.js +1339 -0
  163. package/dist/stack/stack.js.map +1 -0
  164. package/dist/stack/system-types.d.ts +16 -0
  165. package/dist/stack/system-types.d.ts.map +1 -0
  166. package/dist/stack/system-types.js +111 -0
  167. package/dist/stack/system-types.js.map +1 -0
  168. package/dist/stack/type-grants.d.ts +20 -0
  169. package/dist/stack/type-grants.d.ts.map +1 -0
  170. package/dist/stack/type-grants.js +69 -0
  171. package/dist/stack/type-grants.js.map +1 -0
  172. package/dist/stack/write-expectation.d.ts +79 -0
  173. package/dist/stack/write-expectation.d.ts.map +1 -0
  174. package/dist/stack/write-expectation.js +71 -0
  175. package/dist/stack/write-expectation.js.map +1 -0
  176. package/dist/stack/write-gate.d.ts +23 -0
  177. package/dist/stack/write-gate.d.ts.map +1 -0
  178. package/dist/stack/write-gate.js +17 -0
  179. package/dist/stack/write-gate.js.map +1 -0
  180. package/dist/testing.d.ts +44 -33
  181. package/dist/testing.d.ts.map +1 -1
  182. package/dist/testing.js +180 -176
  183. package/dist/testing.js.map +1 -1
  184. package/dist/type-handle.d.ts +67 -78
  185. package/dist/type-handle.d.ts.map +1 -1
  186. package/dist/type-handle.js +44 -111
  187. package/dist/type-handle.js.map +1 -1
  188. package/dist/types/adapter.d.ts +282 -0
  189. package/dist/types/adapter.d.ts.map +1 -0
  190. package/dist/types/adapter.js +5 -0
  191. package/dist/types/adapter.js.map +1 -0
  192. package/dist/types/associations.d.ts +140 -0
  193. package/dist/types/associations.d.ts.map +1 -0
  194. package/dist/types/associations.js +6 -0
  195. package/dist/types/associations.js.map +1 -0
  196. package/dist/types/capabilities.d.ts +89 -0
  197. package/dist/types/capabilities.d.ts.map +1 -0
  198. package/dist/types/capabilities.js +5 -0
  199. package/dist/types/capabilities.js.map +1 -0
  200. package/dist/types/events.d.ts +150 -0
  201. package/dist/types/events.d.ts.map +1 -0
  202. package/dist/types/events.js +5 -0
  203. package/dist/types/events.js.map +1 -0
  204. package/dist/types/ids.d.ts +43 -0
  205. package/dist/types/ids.d.ts.map +1 -0
  206. package/dist/types/ids.js +5 -0
  207. package/dist/types/ids.js.map +1 -0
  208. package/dist/types/index.d.ts +19 -0
  209. package/dist/types/index.d.ts.map +1 -0
  210. package/dist/types/index.js +19 -0
  211. package/dist/types/index.js.map +1 -0
  212. package/dist/types/journal.d.ts +47 -0
  213. package/dist/types/journal.d.ts.map +1 -0
  214. package/dist/types/journal.js +5 -0
  215. package/dist/types/journal.js.map +1 -0
  216. package/dist/types/query.d.ts +185 -0
  217. package/dist/types/query.d.ts.map +1 -0
  218. package/dist/types/query.js +10 -0
  219. package/dist/types/query.js.map +1 -0
  220. package/dist/types/records.d.ts +76 -0
  221. package/dist/types/records.d.ts.map +1 -0
  222. package/dist/types/records.js +5 -0
  223. package/dist/types/records.js.map +1 -0
  224. package/dist/types/schema.d.ts +74 -0
  225. package/dist/types/schema.d.ts.map +1 -0
  226. package/dist/types/schema.js +5 -0
  227. package/dist/types/schema.js.map +1 -0
  228. package/dist/types/system.d.ts +161 -0
  229. package/dist/types/system.d.ts.map +1 -0
  230. package/dist/types/system.js +32 -0
  231. package/dist/types/system.js.map +1 -0
  232. package/dist/types/tokens.d.ts +39 -0
  233. package/dist/types/tokens.d.ts.map +1 -0
  234. package/dist/types/tokens.js +5 -0
  235. package/dist/types/tokens.js.map +1 -0
  236. package/dist/validate.d.ts +31 -70
  237. package/dist/validate.d.ts.map +1 -1
  238. package/dist/validate.js +52 -92
  239. package/dist/validate.js.map +1 -1
  240. package/dist/{attachment-download.d.ts → wire/attachment-download.d.ts} +0 -57
  241. package/dist/wire/attachment-download.d.ts.map +1 -0
  242. package/dist/{attachment-download.js → wire/attachment-download.js} +0 -61
  243. package/dist/wire/attachment-download.js.map +1 -0
  244. package/dist/{auth.d.ts → wire/auth.d.ts} +5 -8
  245. package/dist/wire/auth.d.ts.map +1 -0
  246. package/dist/{auth.js → wire/auth.js} +5 -8
  247. package/dist/wire/auth.js.map +1 -0
  248. package/dist/{wire-body.d.ts → wire/body.d.ts} +35 -12
  249. package/dist/wire/body.d.ts.map +1 -0
  250. package/dist/{wire-body.js → wire/body.js} +74 -13
  251. package/dist/wire/body.js.map +1 -0
  252. package/dist/{wire-entry.d.ts → wire/index.d.ts} +12 -12
  253. package/dist/wire/index.d.ts.map +1 -0
  254. package/dist/{wire-entry.js → wire/index.js} +13 -17
  255. package/dist/wire/index.js.map +1 -0
  256. package/dist/wire/record.d.ts +31 -0
  257. package/dist/wire/record.d.ts.map +1 -0
  258. package/dist/{wire-record.js → wire/record.js} +7 -67
  259. package/dist/wire/record.js.map +1 -0
  260. package/dist/{wire-request.d.ts → wire/request.d.ts} +32 -49
  261. package/dist/wire/request.d.ts.map +1 -0
  262. package/dist/{wire-request.js → wire/request.js} +113 -83
  263. package/dist/wire/request.js.map +1 -0
  264. package/package.json +7 -7
  265. package/dist/adapter-entry.d.ts +0 -16
  266. package/dist/adapter-entry.d.ts.map +0 -1
  267. package/dist/adapter-entry.js +0 -14
  268. package/dist/adapter-entry.js.map +0 -1
  269. package/dist/attachment-download.d.ts.map +0 -1
  270. package/dist/attachment-download.js.map +0 -1
  271. package/dist/auth.d.ts.map +0 -1
  272. package/dist/auth.js.map +0 -1
  273. package/dist/changes.d.ts.map +0 -1
  274. package/dist/changes.js.map +0 -1
  275. package/dist/combine.d.ts.map +0 -1
  276. package/dist/combine.js.map +0 -1
  277. package/dist/did-entry.d.ts.map +0 -1
  278. package/dist/did-entry.js.map +0 -1
  279. package/dist/did.d.ts.map +0 -1
  280. package/dist/did.js.map +0 -1
  281. package/dist/identity-bindings.d.ts.map +0 -1
  282. package/dist/identity-bindings.js.map +0 -1
  283. package/dist/limits.d.ts.map +0 -1
  284. package/dist/limits.js.map +0 -1
  285. package/dist/record-id.d.ts +0 -70
  286. package/dist/record-id.d.ts.map +0 -1
  287. package/dist/record-id.js +0 -127
  288. package/dist/record-id.js.map +0 -1
  289. package/dist/scoped-stack.d.ts +0 -549
  290. package/dist/scoped-stack.d.ts.map +0 -1
  291. package/dist/scoped-stack.js +0 -1301
  292. package/dist/scoped-stack.js.map +0 -1
  293. package/dist/sort.d.ts.map +0 -1
  294. package/dist/sort.js.map +0 -1
  295. package/dist/stack-reads.d.ts +0 -38
  296. package/dist/stack-reads.d.ts.map +0 -1
  297. package/dist/stack-reads.js +0 -79
  298. package/dist/stack-reads.js.map +0 -1
  299. package/dist/stack.d.ts +0 -1032
  300. package/dist/stack.d.ts.map +0 -1
  301. package/dist/stack.js +0 -2337
  302. package/dist/stack.js.map +0 -1
  303. package/dist/types.d.ts +0 -1278
  304. package/dist/types.d.ts.map +0 -1
  305. package/dist/types.js +0 -51
  306. package/dist/types.js.map +0 -1
  307. package/dist/wire-body.d.ts.map +0 -1
  308. package/dist/wire-body.js.map +0 -1
  309. package/dist/wire-entry.d.ts.map +0 -1
  310. package/dist/wire-entry.js.map +0 -1
  311. package/dist/wire-record.d.ts +0 -50
  312. package/dist/wire-record.d.ts.map +0 -1
  313. package/dist/wire-record.js.map +0 -1
  314. package/dist/wire-request.d.ts.map +0 -1
  315. package/dist/wire-request.js.map +0 -1
  316. /package/dist/{did.d.ts → did/did.d.ts} +0 -0
  317. /package/dist/{identity-bindings.d.ts → stack/identity-bindings.d.ts} +0 -0
package/dist/testing.d.ts CHANGED
@@ -1,10 +1,20 @@
1
- import type { StackAdapter, StackRecord, StackType, TypeId, RecordVersion, RecordJournalEntry, JournalOptions, JournalQuery, VersionsQuery, RecordChangeSet, ActorOptions, StackQuery, QueryResult, AssociationEdit, AuthorityAssociation, DataAssociation, StackCapabilities, BlobInfo } from './types.js';
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 offset-based cursor pagination. Implements
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("./types.js").RecordId;
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("./types.js").RecordId;
37
- appId?: import("./types.js").AppId;
38
- createdBy?: import("./types.js").Actor;
39
- updatedBy?: import("./types.js").Actor;
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
- mutateRecord(id: string, changes: RecordChangeSet, opts?: {
63
- ifVersion?: number;
64
- snapshot?: RecordVersion;
65
- bumpsVersion?: boolean;
66
- } & ActorOptions & JournalOptions): Promise<StackRecord>;
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
- ifVersion?: number;
70
- snapshot?: RecordVersion;
71
- } & ActorOptions & JournalOptions): Promise<StackRecord | null>;
72
- undeleteRecord(id: string, opts?: {
73
- ifVersion?: number;
74
- snapshot?: RecordVersion;
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
- * Stringified offset into insertion order, paired with the sort it was
82
- * minted under — mirroring the real adapters' cursor stability guarantee.
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
- ifVersion?: number;
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
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
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
- import { SYSTEM_TYPES } from './types.js';
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 { compareSortEntries, contentSortEntry } from './sort.js';
4
- import { StackVersionConflictError, StackConflictError, StackNotFoundError, StackBadRequestError, } from './errors.js';
5
- import { parseContentFilterKey } from './query-validation.js';
6
- import { applyAssociationEdits, assertOneSurface, associationEqual } from './record-changes.js';
7
- /** Core resolves the default direction; an adapter only ever sees an explicit one. */
8
- const requireDirection = (sort) => {
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 offset-based cursor pagination. Implements
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 Object.entries(schema).some(([field, def]) => def.kind === 'file-ref' && content[field] === fileId);
111
+ return fileRefFields(schema).some((field) => content[field] === fileId);
133
112
  }
134
- async mutateRecord(id, changes, opts = {}) {
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, opts.ifVersion);
139
- if (opts.snapshot)
140
- this.snapshotBeforeMutation(id, opts.snapshot);
141
- // Destructured out so each optional native field is re-added only when
142
- // the change set leaves it set — assigning `undefined` would leave the
143
- // key present, which is how a record grows a `parentId: undefined` that
144
- // every `in` check then reads as a container.
145
- const { parentId: _p, unlistedAt: _u, ...rest } = existing;
146
- let next = rest;
147
- if (changes.contentPatch) {
148
- next = { ...next, content: applyMergePatch(next.content, changes.contentPatch) };
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
- // Each key replaces only within its own half of the partition, which
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
- /** Cursor is a stringified offset into insertion order. */
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
- // A `null` filter value means "field absent or null" — not "match
275
- // nothing". Plain `===` would miss an absent field; treat both as
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
- results = this.sortRecords(results, query.sort);
288
- // Element-wise like the content filter above: a path holds a value
289
- // when at least one non-null value is reachable at it.
290
- if (f.contentPresent?.length) {
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
- const limit = query.limit ?? 50;
295
- const start = query.cursor ? this.decodeCursor(query.cursor, query.sort) : 0;
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 nextStart = start + limit;
298
- const cursor = nextStart < results.length ? this.encodeCursor(query.sort, nextStart) : null;
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
- return JSON.stringify([
304
- sort?.contentField ?? null,
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
- * Stringified offset into insertion order, paired with the sort it was
311
- * minted under — mirroring the real adapters' cursor stability guarantee.
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, offset) {
315
- const json = JSON.stringify({ d: this.sortDescriptor(sort), o: offset });
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, o } = (parsed ?? {});
345
- // `o >= 0` is part of being well-formed, not a nicety: a crafted
346
- // negative offset would reach `results.slice(-3, ...)` and hand back
347
- // the tail of the result set as "the next page". Cursors arrive from
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
- return o;
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 direction = sort ? requireDirection(sort) : 'desc';
367
- const sign = direction === 'asc' ? 1 : -1;
368
- const byId = (a, b) => (a.id < b.id ? -sign : a.id > b.id ? sign : 0);
369
- if (sort?.contentField === undefined) {
370
- const field = sort?.field ?? 'createdAt';
371
- const value = (r) => field === 'version'
372
- ? r.version
373
- : (field === 'updatedAt' ? r.updatedAt : r.createdAt).getTime();
374
- return [...records].sort((a, b) => sign * (value(a) - value(b)) || byId(a, b));
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
- const field = sort.contentField;
377
- const entries = new Map(records.map((r) => [r.id, this.sortEntry(r, field)]));
378
- return [...records].sort((a, b) => compareSortEntries(entries.get(a.id), entries.get(b.id), direction) || byId(a, b));
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, field) {
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 an association carries authority — mirrors core's own predicate. */
588
- const isAuthority = (a) => a.kind === 'permission' || a.kind === 'anyone';
589
- /**
590
- * A record's whole association table, both projections rejoined — what
591
- * amendAssociations() acts on, since storage keys all kinds alike.
592
- */
593
- function allAssociations(record) {
594
- return [...(record.associations ?? []), ...(record.permissions ?? [])];
595
- }
596
- /**
597
- * Sets a record's association set, keyed by identity as every adapter's
598
- * association table is: a list naming one identity twice collapses to one
599
- * entry, last wins. Core refuses such a list before an adapter sees it —
600
- * this is what keeps a direct caller from reaching a state a SQL store
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
- /** withAssociationSet() over the authority half alone — the `permissions` key. */
624
- function withPermissions(record, permissions) {
625
- return withAssociationSet(record, [...(record.associations ?? []), ...permissions]);
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