create-qpq-app 0.1.11 → 0.1.13

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 (194) hide show
  1. package/lib/commonjs/cli/runCreateQpqApp.js +14 -12
  2. package/lib/commonjs/cli/runCreateQpqApp.js.map +1 -1
  3. package/lib/commonjs/lib/getArgValue.d.ts +1 -0
  4. package/lib/commonjs/lib/getArgValue.js +14 -0
  5. package/lib/commonjs/lib/getArgValue.js.map +1 -0
  6. package/lib/commonjs/lib/{packageRoot.d.ts → getOwnPackageRoot.d.ts} +0 -1
  7. package/lib/commonjs/lib/{packageRoot.js → getOwnPackageRoot.js} +2 -7
  8. package/lib/commonjs/lib/getOwnPackageRoot.js.map +1 -0
  9. package/lib/commonjs/lib/getOwnVersion.d.ts +1 -0
  10. package/lib/commonjs/lib/getOwnVersion.js +15 -0
  11. package/lib/commonjs/lib/getOwnVersion.js.map +1 -0
  12. package/lib/commonjs/lib/{args.d.ts → getPositionalArgs.d.ts} +0 -1
  13. package/lib/commonjs/lib/getPositionalArgs.js +12 -0
  14. package/lib/commonjs/lib/getPositionalArgs.js.map +1 -0
  15. package/lib/commonjs/lib/index.d.ts +11 -0
  16. package/lib/commonjs/lib/index.js +28 -0
  17. package/lib/commonjs/lib/index.js.map +1 -0
  18. package/lib/commonjs/lib/listFilesRecursive.d.ts +1 -0
  19. package/lib/commonjs/lib/listFilesRecursive.js +27 -0
  20. package/lib/commonjs/lib/listFilesRecursive.js.map +1 -0
  21. package/lib/commonjs/lib/{prompts.js → promptSelect.js} +1 -1
  22. package/lib/commonjs/lib/promptSelect.js.map +1 -0
  23. package/lib/commonjs/lib/readJsonFile.d.ts +1 -0
  24. package/lib/commonjs/lib/readJsonFile.js +13 -0
  25. package/lib/commonjs/lib/readJsonFile.js.map +1 -0
  26. package/lib/commonjs/lib/replaceInFileExact.d.ts +1 -0
  27. package/lib/commonjs/lib/replaceInFileExact.js +18 -0
  28. package/lib/commonjs/lib/replaceInFileExact.js.map +1 -0
  29. package/lib/commonjs/lib/replaceInFiles.d.ts +1 -0
  30. package/lib/commonjs/lib/replaceInFiles.js +32 -0
  31. package/lib/commonjs/lib/replaceInFiles.js.map +1 -0
  32. package/lib/commonjs/lib/writeJsonFile.d.ts +1 -0
  33. package/lib/commonjs/lib/writeJsonFile.js +12 -0
  34. package/lib/commonjs/lib/writeJsonFile.js.map +1 -0
  35. package/lib/commonjs/steps/001_preflight.js +3 -3
  36. package/lib/commonjs/steps/001_preflight.js.map +1 -1
  37. package/lib/commonjs/steps/002_copyTemplate.js +1 -1
  38. package/lib/commonjs/steps/002_copyTemplate.js.map +1 -1
  39. package/lib/commonjs/steps/003_deleteDocusaurus.js +5 -4
  40. package/lib/commonjs/steps/003_deleteDocusaurus.js.map +1 -1
  41. package/lib/commonjs/steps/005_applyAppIdentity.js +12 -9
  42. package/lib/commonjs/steps/005_applyAppIdentity.js.map +1 -1
  43. package/lib/commonjs/steps/006_applyDomain.js +6 -4
  44. package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
  45. package/lib/commonjs/steps/007_pinRegistryVersions.js +5 -4
  46. package/lib/commonjs/steps/007_pinRegistryVersions.js.map +1 -1
  47. package/lib/commonjs/steps/008_transpileToJavaScript.js +19 -17
  48. package/lib/commonjs/steps/008_transpileToJavaScript.js.map +1 -1
  49. package/lib/commonjs/steps/009_restoreGitignore.js +2 -2
  50. package/lib/commonjs/steps/009_restoreGitignore.js.map +1 -1
  51. package/lib/commonjs/steps/010_gitInit.js +1 -1
  52. package/lib/commonjs/steps/010_gitInit.js.map +1 -1
  53. package/lib/commonjs/steps/013_printNextSteps.js +1 -1
  54. package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
  55. package/lib/commonjs/steps/index.js +1 -1
  56. package/lib/commonjs/steps/index.js.map +1 -1
  57. package/lib/commonjs/types/AppLanguage.d.ts +4 -0
  58. package/lib/commonjs/{types.js → types/AppLanguage.js} +1 -1
  59. package/lib/commonjs/types/AppLanguage.js.map +1 -0
  60. package/lib/commonjs/types/CreateQpqAppAnswers.d.ts +8 -0
  61. package/lib/commonjs/types/CreateQpqAppAnswers.js +3 -0
  62. package/lib/commonjs/types/CreateQpqAppAnswers.js.map +1 -0
  63. package/lib/commonjs/types/CreateQpqAppStep.d.ts +7 -0
  64. package/lib/commonjs/types/CreateQpqAppStep.js +3 -0
  65. package/lib/commonjs/types/CreateQpqAppStep.js.map +1 -0
  66. package/lib/commonjs/types/StepContext.d.ts +7 -0
  67. package/lib/commonjs/types/StepContext.js +3 -0
  68. package/lib/commonjs/types/StepContext.js.map +1 -0
  69. package/lib/commonjs/types/index.d.ts +4 -0
  70. package/lib/commonjs/types/index.js +21 -0
  71. package/lib/commonjs/types/index.js.map +1 -0
  72. package/lib/esm/cli/runCreateQpqApp.js +8 -6
  73. package/lib/esm/cli/runCreateQpqApp.js.map +1 -1
  74. package/lib/esm/lib/getArgValue.d.ts +1 -0
  75. package/lib/esm/lib/getArgValue.js +10 -0
  76. package/lib/esm/lib/getArgValue.js.map +1 -0
  77. package/lib/esm/lib/{packageRoot.d.ts → getOwnPackageRoot.d.ts} +0 -1
  78. package/lib/esm/lib/{packageRoot.js → getOwnPackageRoot.js} +1 -5
  79. package/lib/esm/lib/getOwnPackageRoot.js.map +1 -0
  80. package/lib/esm/lib/getOwnVersion.d.ts +1 -0
  81. package/lib/esm/lib/getOwnVersion.js +8 -0
  82. package/lib/esm/lib/getOwnVersion.js.map +1 -0
  83. package/lib/esm/lib/{args.d.ts → getPositionalArgs.d.ts} +0 -1
  84. package/lib/esm/lib/getPositionalArgs.js +8 -0
  85. package/lib/esm/lib/getPositionalArgs.js.map +1 -0
  86. package/lib/esm/lib/index.d.ts +11 -0
  87. package/lib/esm/lib/index.js +12 -0
  88. package/lib/esm/lib/index.js.map +1 -0
  89. package/lib/esm/lib/listFilesRecursive.d.ts +1 -0
  90. package/lib/esm/lib/listFilesRecursive.js +20 -0
  91. package/lib/esm/lib/listFilesRecursive.js.map +1 -0
  92. package/lib/esm/lib/{prompts.js → promptSelect.js} +1 -1
  93. package/lib/esm/lib/promptSelect.js.map +1 -0
  94. package/lib/esm/lib/readJsonFile.d.ts +1 -0
  95. package/lib/esm/lib/readJsonFile.js +6 -0
  96. package/lib/esm/lib/readJsonFile.js.map +1 -0
  97. package/lib/esm/lib/replaceInFileExact.d.ts +1 -0
  98. package/lib/esm/lib/replaceInFileExact.js +11 -0
  99. package/lib/esm/lib/replaceInFileExact.js.map +1 -0
  100. package/lib/esm/lib/replaceInFiles.d.ts +1 -0
  101. package/lib/esm/lib/replaceInFiles.js +25 -0
  102. package/lib/esm/lib/replaceInFiles.js.map +1 -0
  103. package/lib/esm/lib/writeJsonFile.d.ts +1 -0
  104. package/lib/esm/lib/writeJsonFile.js +5 -0
  105. package/lib/esm/lib/writeJsonFile.js.map +1 -0
  106. package/lib/esm/steps/001_preflight.js +3 -3
  107. package/lib/esm/steps/001_preflight.js.map +1 -1
  108. package/lib/esm/steps/002_copyTemplate.js +1 -1
  109. package/lib/esm/steps/002_copyTemplate.js.map +1 -1
  110. package/lib/esm/steps/003_deleteDocusaurus.js +3 -2
  111. package/lib/esm/steps/003_deleteDocusaurus.js.map +1 -1
  112. package/lib/esm/steps/005_applyAppIdentity.js +6 -3
  113. package/lib/esm/steps/005_applyAppIdentity.js.map +1 -1
  114. package/lib/esm/steps/006_applyDomain.js +3 -1
  115. package/lib/esm/steps/006_applyDomain.js.map +1 -1
  116. package/lib/esm/steps/007_pinRegistryVersions.js +3 -2
  117. package/lib/esm/steps/007_pinRegistryVersions.js.map +1 -1
  118. package/lib/esm/steps/008_transpileToJavaScript.js +10 -8
  119. package/lib/esm/steps/008_transpileToJavaScript.js.map +1 -1
  120. package/lib/esm/steps/009_restoreGitignore.js +1 -1
  121. package/lib/esm/steps/009_restoreGitignore.js.map +1 -1
  122. package/lib/esm/steps/010_gitInit.js +1 -1
  123. package/lib/esm/steps/010_gitInit.js.map +1 -1
  124. package/lib/esm/steps/013_printNextSteps.js +1 -1
  125. package/lib/esm/steps/013_printNextSteps.js.map +1 -1
  126. package/lib/esm/steps/index.js +1 -1
  127. package/lib/esm/steps/index.js.map +1 -1
  128. package/lib/esm/types/AppLanguage.d.ts +4 -0
  129. package/lib/esm/{types.js → types/AppLanguage.js} +1 -1
  130. package/lib/esm/types/AppLanguage.js.map +1 -0
  131. package/lib/esm/types/CreateQpqAppAnswers.d.ts +8 -0
  132. package/lib/esm/types/CreateQpqAppAnswers.js +2 -0
  133. package/lib/esm/types/CreateQpqAppAnswers.js.map +1 -0
  134. package/lib/esm/types/CreateQpqAppStep.d.ts +7 -0
  135. package/lib/esm/types/CreateQpqAppStep.js +2 -0
  136. package/lib/esm/types/CreateQpqAppStep.js.map +1 -0
  137. package/lib/esm/types/StepContext.d.ts +7 -0
  138. package/lib/esm/types/StepContext.js +2 -0
  139. package/lib/esm/types/StepContext.js.map +1 -0
  140. package/lib/esm/types/index.d.ts +4 -0
  141. package/lib/esm/types/index.js +5 -0
  142. package/lib/esm/types/index.js.map +1 -0
  143. package/package.json +5 -4
  144. package/template/docusaurus/docs/actions/core/crypto/_category_.json +7 -0
  145. package/template/docusaurus/docs/actions/core/crypto/ask-crypto-decrypt.md +61 -0
  146. package/template/docusaurus/docs/actions/core/crypto/ask-crypto-encrypt.md +66 -0
  147. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-scan-all-scopes.md +98 -0
  148. package/template/docusaurus/docs/actions/features/_category_.json +1 -1
  149. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +2 -2
  150. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +2 -2
  151. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +17 -15
  152. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +9 -9
  153. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +7 -7
  154. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +5 -5
  155. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +4 -4
  156. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-provide-store.md +6 -0
  157. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-references.md +47 -0
  158. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-soft-delete.md +48 -10
  159. package/template/docusaurus/docs/actions/features/event-doc-transfer/_category_.json +1 -0
  160. package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-bundle-apply.md +67 -0
  161. package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-bundle-plan.md +67 -0
  162. package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-manifest.md +64 -0
  163. package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-transfer-export.md +67 -0
  164. package/template/docusaurus/docs/actions/xstate/ask-state-machine-send-event.md +6 -6
  165. package/template/docusaurus/docs/config/core/crypto-key.md +53 -0
  166. package/template/docusaurus/docs/config/core/key-value-store.md +53 -1
  167. package/template/docusaurus/docs/config/features/event-doc-routes.md +5 -1
  168. package/template/docusaurus/docs/config/features/event-doc-summary.md +7 -6
  169. package/template/docusaurus/docs/config/features/event-doc-transfer.md +81 -0
  170. package/template/docusaurus/docs/config/features/event-doc.md +1 -0
  171. package/template/docusaurus/docs/config/features/tenanted-event-doc-transfer.md +59 -0
  172. package/template/docusaurus/docs/config/features/tenanted-event-doc.md +1 -0
  173. package/template/docusaurus/docs/config/webserver/migration.md +4 -0
  174. package/template/package.json +1 -0
  175. package/lib/commonjs/lib/args.js +0 -21
  176. package/lib/commonjs/lib/args.js.map +0 -1
  177. package/lib/commonjs/lib/files.d.ts +0 -5
  178. package/lib/commonjs/lib/files.js +0 -65
  179. package/lib/commonjs/lib/files.js.map +0 -1
  180. package/lib/commonjs/lib/packageRoot.js.map +0 -1
  181. package/lib/commonjs/lib/prompts.js.map +0 -1
  182. package/lib/commonjs/types.d.ts +0 -22
  183. package/lib/commonjs/types.js.map +0 -1
  184. package/lib/esm/lib/args.js +0 -16
  185. package/lib/esm/lib/args.js.map +0 -1
  186. package/lib/esm/lib/files.d.ts +0 -5
  187. package/lib/esm/lib/files.js +0 -54
  188. package/lib/esm/lib/files.js.map +0 -1
  189. package/lib/esm/lib/packageRoot.js.map +0 -1
  190. package/lib/esm/lib/prompts.js.map +0 -1
  191. package/lib/esm/types.d.ts +0 -22
  192. package/lib/esm/types.js.map +0 -1
  193. /package/lib/commonjs/lib/{prompts.d.ts → promptSelect.d.ts} +0 -0
  194. /package/lib/esm/lib/{prompts.d.ts → promptSelect.d.ts} +0 -0
@@ -19,6 +19,8 @@ This is the same pattern as [askContextProvideValue](../../core/context/ask-cont
19
19
  | `storageDriveName` | `string` | The collection's blob bucket (assets + runtime artifacts), keyed per-doc. |
20
20
  | `eventValidator` | `string` (optional) | The collection's append-time validator inline-function name, if configured. |
21
21
  | `eventRenderer` | `string` (optional) | The collection's render inline-function name, if configured (powers `GET .../render`). |
22
+ | `scopeResolver` | `string` (optional) | The collection's ambient storage scope resolver inline-function name, if configured (e.g. per-tenant). |
23
+ | `referenceResolver` | `string` (optional) | The collection's reference-collector inline-function name, if configured (powers `GET .../references` and the transfer feature's manifest walk). |
22
24
 
23
25
  There are two ways to establish the context — one for custom routes, one for the built-in routes — plus the raw provide/read primitives and a resolver that throws when the binding is missing.
24
26
 
@@ -64,6 +66,10 @@ function* askEventDocProvideStore<T>(
64
66
  | `type` | `string` | The document type pinned within the store. |
65
67
  | `eventValidator` | `string` (optional) | Append-time validator inline-function name. |
66
68
  | `eventRenderer` | `string` (optional) | Render inline-function name. |
69
+ | `onPublish` | `string` (optional) | Inline-function name invoked after a Publish append. |
70
+ | `onAppend` | `string` (optional) | Inline-function name invoked after every append. |
71
+ | `scopeResolver` | `string` (optional) | Ambient storage scope resolver inline-function name. |
72
+ | `referenceResolver` | `string` (optional) | Reference-collector inline-function name (powers `GET .../references`). |
67
73
 
68
74
  ### Returns
69
75
 
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: askEventDocReferences
3
+ description: The other docs one document depends on, one hop out, via its collection's referenceResolver.
4
+ ---
5
+
6
+ # askEventDocReferences
7
+
8
+ Reads the `EventDocLink`s a document depends on, one hop out. Hands the collection's `referenceResolver` inline function (see [defineEventDocRoutes](../../../config/features/event-doc-routes.md#parameters)) the document's whole event log and lets it fold + walk it. A collection with no resolver configured is a leaf: this returns `[]` without reading the log at all.
9
+
10
+ - **Built from:** [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) (to read the collection's `referenceResolver` name) and `askEventDocEventListAll` (the full log the resolver folds). Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
11
+
12
+ ```typescript
13
+ import { askEventDocReferences } from 'quidproquo-features';
14
+
15
+ export function* readTemplateDependencies(templateId: string) {
16
+ const links = yield* askEventDocReferences(templateId);
17
+
18
+ return links; // EventDocLink[] — e.g. the template's layout, styles, and content
19
+ }
20
+ ```
21
+
22
+ ## Signature
23
+
24
+ ```typescript
25
+ function* askEventDocReferences(docId: string): AskResponse<EventDocLink[]>;
26
+ ```
27
+
28
+ ## Parameters
29
+
30
+ | Parameter | Type | Description |
31
+ | --- | --- | --- |
32
+ | `docId` | `string` | The document to read outbound references for. |
33
+
34
+ ## Returns
35
+
36
+ `EventDocLink[]` — the doc's outbound links, one hop out. Empty when the collection has no `referenceResolver` configured.
37
+
38
+ ## Notes
39
+
40
+ - This is a **one-hop** read. The recursive walk over these edges (following a template into its content, then that content's own references, and so on) is the transfer feature's job — see [askEventDocManifest](../event-doc-transfer/ask-event-doc-manifest.md).
41
+ - `GET {basePath}/{id}/references`, mounted by [defineEventDocRoutes](../../../config/features/event-doc-routes.md), calls this for one document; the route is always mounted, resolving to `[]` when no `referenceResolver` is configured.
42
+
43
+ ## Related
44
+
45
+ - [defineEventDocRoutes](../../../config/features/event-doc-routes.md) — declares the `referenceResolver` this reads.
46
+ - [askEventDocManifest](../event-doc-transfer/ask-event-doc-manifest.md) — the recursive walk built on top of this, one collection at a time.
47
+ - [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md) — read the document's own summary alongside its references.
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  title: askEventDocSoftDelete
3
- description: Soft-delete an event document by stamping deletedAt, keeping its versions and assets intact.
3
+ description: Soft-delete an event document by appending a DELETE event, keeping its versions and assets intact — and restore it with askEventDocRestore.
4
4
  ---
5
5
 
6
6
  # askEventDocSoftDelete
7
7
 
8
- Soft-deletes an event document by stamping `deletedAt` (and refreshing `updatedAt`/`updatedBy`) on its summary record. The document's versions and blob claims stay intact — nothing is destroyed — and [askEventDocList](./ask-event-doc-list.md) hides it by default. This is the **public** deletion path. Returns the updated [`EventDocSummary`](./ask-event-doc-get-by-id.md#the-summary-record).
8
+ Soft-deletes an event document by appending a reserved `DELETE` event to its log. `deletedAt` on the summary record is derived from that event by the fold (not written directly), and [askEventDocList](./ask-event-doc-list.md) hides the document by default once it's set. The document's versions and blob claims stay intact — nothing is destroyed. This is the **public** deletion path. Returns the updated [`EventDocSummary`](./ask-event-doc-get-by-id.md#the-summary-record).
9
9
 
10
- - **Built from:** [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md#askeventdocgetbyidorthrow) (loads the record, throwing `NotFound` if missing), then a validated [askEventDocUpsert](./ask-event-doc-create.md#askeventdocupsert). Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`.
10
+ - **Built from:** [askEventDocAppendServerEvent](./ask-event-doc-append-server-event.md) (appends the `DELETE` event) then [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md#askeventdocgetbyidorthrow) (re-reads the re-derived record). Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`.
11
11
 
12
12
  ```typescript
13
13
  import { askEventDocSoftDelete } from 'quidproquo-features';
14
14
 
15
- export function* archiveArticle(id: string, userId: string) {
16
- const summary = yield* askEventDocSoftDelete(id, userId);
15
+ export function* archiveArticle(id: string, userId: string, schemaVersion: number) {
16
+ const summary = yield* askEventDocSoftDelete(id, userId, schemaVersion);
17
17
  return summary; // deletedAt is now set
18
18
  }
19
19
  ```
@@ -24,6 +24,7 @@ export function* archiveArticle(id: string, userId: string) {
24
24
  function* askEventDocSoftDelete(
25
25
  id: string,
26
26
  updatedBy: string,
27
+ schemaVersion: number,
27
28
  ): AskResponse<EventDocSummary>;
28
29
  ```
29
30
 
@@ -32,19 +33,55 @@ function* askEventDocSoftDelete(
32
33
  | Parameter | Type | Description |
33
34
  | --- | --- | --- |
34
35
  | `id` | `string` | Id of the document to soft-delete. |
35
- | `updatedBy` | `string` | User id to record as having performed the deletion (written to `updatedBy`). |
36
+ | `updatedBy` | `string` | User id to record as having authored the `DELETE` event (used as both `userId` and `userDisplayName` on the event's actor). |
37
+ | `schemaVersion` | `number` | The schema version to stamp the `DELETE` event with, same as any other event — the fold rejects an event authored against an older schema than the log has already reached. |
36
38
 
37
39
  ## Returns
38
40
 
39
- `EventDocSummary` — the updated record with `deletedAt` set to the deletion time.
41
+ `EventDocSummary` — the re-derived record, with `deletedAt` set to the `DELETE` event's time.
40
42
 
41
43
  ## Notes
42
44
 
43
- - Throws `ErrorTypeEnum.NotFound` (from quidproquo-core) when no document exists for `id`.
45
+ - The reserved `DELETE` validator (`requireNotDeleted`) rejects a `DELETE` on an already-deleted document — but rejection happens at fold time, not at append (see [askEventDocEventAppend](./ask-event-doc-event-append.md)), so calling this on an already-deleted document does not throw: the event is written, the fold silently skips it, and the re-derived summary comes back unchanged.
44
46
  - Soft-deleted rows are still returned by [askEventDocGetById](./ask-event-doc-get-by-id.md) (filtering is the caller's concern) and by [askEventDocList](./ask-event-doc-list.md) only when `includeDeleted: true`.
45
47
 
46
48
  ---
47
49
 
50
+ ## askEventDocRestore
51
+
52
+ Undoes a soft delete by appending a reserved `RESTORE` event. The `DELETE` event stays in the log — history is append-only, so the deletion remains auditable — but the fold stops treating the document as deleted from this point on: `deletedAt` is cleared on the re-derived summary record, and the document goes back into default (non-`includeDeleted`) listings. Everything else (versions, blob claims) is untouched, so the document comes back exactly as it was. Returns the updated `EventDocSummary`.
53
+
54
+ ```typescript
55
+ import { askEventDocRestore } from 'quidproquo-features';
56
+
57
+ export function* unarchiveArticle(id: string, userId: string, schemaVersion: number) {
58
+ const summary = yield* askEventDocRestore(id, userId, schemaVersion);
59
+ return summary; // deletedAt is cleared
60
+ }
61
+ ```
62
+
63
+ ### Signature
64
+
65
+ ```typescript
66
+ function* askEventDocRestore(
67
+ id: string,
68
+ updatedBy: string,
69
+ schemaVersion: number,
70
+ ): AskResponse<EventDocSummary>;
71
+ ```
72
+
73
+ | Parameter | Type | Description |
74
+ | --- | --- | --- |
75
+ | `id` | `string` | Id of the document to restore. |
76
+ | `updatedBy` | `string` | User id to record as having authored the `RESTORE` event. |
77
+ | `schemaVersion` | `number` | The schema version to stamp the `RESTORE` event with, for the same reason as `askEventDocSoftDelete`. |
78
+
79
+ **Returns** `EventDocSummary` — the re-derived record, with `deletedAt` cleared.
80
+
81
+ The reserved `RESTORE` validator (`requireDeleted`) rejects a `RESTORE` on a document that isn't currently deleted — again at fold time, not append, so calling this on a non-deleted document does not throw; it's a silent no-op. Requires the store context, built the same way as `askEventDocSoftDelete` above.
82
+
83
+ ---
84
+
48
85
  ## askEventDocDelete
49
86
 
50
87
  **Hard delete** — permanently removes the document's summary row from the store. This is for internal cleanup/admin only; the public lifecycle uses soft delete (above). It does not touch the event log or the asset bucket, so a hard delete of the summary alone leaves orphaned events/assets — use deliberately.
@@ -63,7 +100,8 @@ Requires the store context. Prefer `askEventDocSoftDelete` for anything user-fac
63
100
 
64
101
  ## Related
65
102
 
66
- - [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md#askeventdocgetbyidorthrow) — the load-or-throw this composes.
103
+ - [askEventDocAppendServerEvent](./ask-event-doc-append-server-event.md) — appends the `DELETE`/`RESTORE` event both actions compose.
104
+ - [askEventDocEventAppend](./ask-event-doc-event-append.md) — the underlying append; explains why rejection is silent rather than thrown.
105
+ - [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md#askeventdocgetbyidorthrow) — reads the re-derived record back.
67
106
  - [askEventDocList](./ask-event-doc-list.md) — hides soft-deleted documents by default.
68
107
  - [askEventDocCreate](./ask-event-doc-create.md) — the create counterpart.
69
- - [askDateNow](../../core/date/ask-date-now.md) — supplies the `deletedAt` timestamp.
@@ -0,0 +1 @@
1
+ { "label": "Event Doc Transfer", "link": { "type": "generated-index", "description": "Stories behind the export/import bundle feature — walk a doc's references into a manifest, stage a bundle, and plan/apply an import against the target's own collections." } }
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: askEventDocBundleApply
3
+ description: Import every doc in a staged bundle, in leaves-first order, and report what happened per doc.
4
+ ---
5
+
6
+ # askEventDocBundleApply
7
+
8
+ Imports every doc in a staged bundle, in bundle order (leaves-first, the order [askEventDocTransferExport](./ask-event-doc-transfer-export.md) wrote it in), and reports what happened per doc in the same row shape [askEventDocBundlePlan](./ask-event-doc-bundle-plan.md) returns.
9
+
10
+ Docs are imported independently: a blocking row (`diverged`, `codeConflict`) is reported and the rest of the bundle still lands. That is deliberate — the alternative, aborting the whole bundle, would mean one hand-edited doc in the target blocks every unrelated doc's promotion. Every write is conditional (on `(docId, index)`, or on an absent asset guid), so re-running after fixing the blocker is safe.
11
+
12
+ - **Built from:** `askEventDocTransferProvideCollection` (runs each doc's import against its own collection's store) then `askEventDocBundleApplyDoc` (the per-doc write). Requires an `EventDocTransferRegistry` and a parsed `EventDocBundle` — the built-in `POST /transfer/import` route resolves both from `{ transferId }`, plus the importing user id, before calling this.
13
+
14
+ ```typescript
15
+ import { askEventDocBundleApply, askEventDocTransferReadBundle, askEventDocTransferReadRegistry } from 'quidproquo-features';
16
+
17
+ export function* applyImport(transferId: string, importerUserId: string, force = false) {
18
+ const registry = yield* askEventDocTransferReadRegistry();
19
+ const bundle = yield* askEventDocTransferReadBundle(transferId);
20
+
21
+ const rows = yield* askEventDocBundleApply(registry, bundle, { transferId, importerUserId, force });
22
+
23
+ return rows; // EventDocTransferPlanRow[] — same shape as a plan, now with eventsWritten/assetsWritten filled in
24
+ }
25
+ ```
26
+
27
+ ## Signature
28
+
29
+ ```typescript
30
+ function* askEventDocBundleApply(
31
+ registry: EventDocTransferRegistry,
32
+ bundle: EventDocBundle,
33
+ options: EventDocBundleApplyOptions,
34
+ ): AskResponse<EventDocTransferPlanRow[]>;
35
+ ```
36
+
37
+ ## Parameters
38
+
39
+ | Parameter | Type | Description |
40
+ | --- | --- | --- |
41
+ | `registry` | `EventDocTransferRegistry` | `{ service, collections }` — which collections the target may import into, as registered by [defineEventDocTransfer](../../../config/features/event-doc-transfer.md). |
42
+ | `bundle` | `EventDocBundle` | The staged bundle to apply, read with `askEventDocTransferReadBundle`. |
43
+ | `options` | `EventDocBundleApplyOptions` | See below. |
44
+
45
+ ### `EventDocBundleApplyOptions`
46
+
47
+ | Property | Type | Description |
48
+ | --- | --- | --- |
49
+ | `transferId` | `string` | Which staged bundle this is, so a discarded (overwritten) tail is parked next to it. |
50
+ | `importerUserId` | `string` | The user id every imported event is attributed to. The source system's user id is deliberately **not** carried over — it resolves to nobody in the target directory — though the author's `userDisplayName` is kept, so history still reads as the person who wrote it. |
51
+ | `force` | `boolean` (optional) | Discard the target's divergent tail and take the bundle's version instead. Off by default and never implicit — it deletes events the target owns and rewrites published version history. Applies only to `diverged` rows; a `codeConflict` is a different problem overwriting cannot fix. |
52
+
53
+ ## Returns
54
+
55
+ `EventDocTransferPlanRow[]` — one row per doc, in bundle order, same shape as [askEventDocBundlePlan](./ask-event-doc-bundle-plan.md#returns) but with `eventsWritten`/`assetsWritten`/`discardedEvents` now reflecting what was actually written.
56
+
57
+ ## Errors
58
+
59
+ | Condition | Error |
60
+ | --- | --- |
61
+ | `bundle.formatVersion` doesn't match this deployment's `EVENT_DOC_TRANSFER_BUNDLE_FORMAT_VERSION` | `ErrorTypeEnum.BadRequest` |
62
+
63
+ ## Related
64
+
65
+ - [askEventDocBundlePlan](./ask-event-doc-bundle-plan.md) — the read-only preview of what this would do.
66
+ - [askEventDocTransferExport](./ask-event-doc-transfer-export.md) — produces the bundle this applies.
67
+ - [defineEventDocTransfer](../../../config/features/event-doc-transfer.md) — mounts `POST /transfer/import`, the HTTP entry point for this story.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: askEventDocBundlePlan
3
+ description: What importing an uploaded bundle would do to each of its docs, without writing anything.
4
+ ---
5
+
6
+ # askEventDocBundlePlan
7
+
8
+ Computes what importing a bundle would do to **each** of its docs, writing nothing. This is the review gate: a UI shows these rows and the operator confirms before anything actually lands. Rows come back in bundle order (leaves-first, the same order [askEventDocBundleApply](./ask-event-doc-bundle-apply.md) applies them in), so the plan and the apply result read the same way.
9
+
10
+ Each doc is compared against the target's own log for the same id (see `findEventDocLogDivergence`) to decide its `EventDocTransferStatus`: a fresh `new` doc, a `fastForward` (the target's log is a strict prefix of the incoming one), an already-`same` no-op, or a blocking `diverged`/`codeConflict` row that nothing is written for unless the caller forces it.
11
+
12
+ - **Built from:** `askEventDocTransferProvideCollection` (runs each doc's comparison against its own collection's store) then `askEventDocBundlePlanDoc` (the per-doc comparison). Requires an `EventDocTransferRegistry` and a parsed `EventDocBundle` — the built-in `POST /transfer/plan` route resolves both from `{ transferId }` before calling this.
13
+
14
+ ```typescript
15
+ import { askEventDocBundlePlan, askEventDocTransferReadBundle, askEventDocTransferReadRegistry } from 'quidproquo-features';
16
+
17
+ export function* reviewImport(transferId: string) {
18
+ const registry = yield* askEventDocTransferReadRegistry();
19
+ const bundle = yield* askEventDocTransferReadBundle(transferId);
20
+
21
+ const rows = yield* askEventDocBundlePlan(registry, bundle);
22
+
23
+ return rows; // EventDocTransferPlanRow[]
24
+ }
25
+ ```
26
+
27
+ ## Signature
28
+
29
+ ```typescript
30
+ function* askEventDocBundlePlan(
31
+ registry: EventDocTransferRegistry,
32
+ bundle: EventDocBundle,
33
+ ): AskResponse<EventDocTransferPlanRow[]>;
34
+ ```
35
+
36
+ ## Parameters
37
+
38
+ | Parameter | Type | Description |
39
+ | --- | --- | --- |
40
+ | `registry` | `EventDocTransferRegistry` | `{ service, collections }` — which collections the target may import into, as registered by [defineEventDocTransfer](../../../config/features/event-doc-transfer.md). |
41
+ | `bundle` | `EventDocBundle` | The staged bundle to evaluate (`{ formatVersion, source, docs }`), read with `askEventDocTransferReadBundle`. |
42
+
43
+ ## Returns
44
+
45
+ `EventDocTransferPlanRow[]` — one row per doc in the bundle, in bundle order:
46
+
47
+ ```typescript
48
+ type EventDocTransferPlanRow = EventDocDocRef & {
49
+ code: string;
50
+ name: string;
51
+ status: EventDocTransferStatus; // 'new' | 'fastForward' | 'same' | 'diverged' | 'codeConflict' | 'overwritten' | 'ignored'
52
+ incomingEvents: number;
53
+ existingEvents: number;
54
+ eventsWritten: number; // always 0 on a plan
55
+ assetsWritten: number; // always 0 on a plan
56
+ discardedEvents: number; // always 0 on a plan
57
+ detail?: string; // why a blocking status blocks
58
+ };
59
+ ```
60
+
61
+ `overwritten` never appears on a plan — it is report-only, produced by a forced [askEventDocBundleApply](./ask-event-doc-bundle-apply.md).
62
+
63
+ ## Related
64
+
65
+ - [askEventDocBundleApply](./ask-event-doc-bundle-apply.md) — applies the same comparison and actually writes.
66
+ - [askEventDocTransferExport](./ask-event-doc-transfer-export.md) — produces the bundle a plan evaluates.
67
+ - [defineEventDocTransfer](../../../config/features/event-doc-transfer.md) — mounts `POST /transfer/plan`, the HTTP entry point for this story.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: askEventDocManifest
3
+ description: Walk one or more docs' references outward to find everything that has to travel with them.
4
+ ---
5
+
6
+ # askEventDocManifest
7
+
8
+ Finds every doc that has to travel with a list of starting docs, by following each doc's `referenceResolver` links outward — breadth-first, across collections, with a visited set so a stylesheet three templates share is walked once and lands in the result once. Also the source of a cycle's termination: a link cycle (template → content → template) stops on the visited check instead of recursing forever.
9
+
10
+ Takes a **list** of roots so selecting several documents produces one merged manifest, rather than one per selection. A soft-deleted doc is reported (`deleted: true`) but not walked into — it will never be bundled, so its own dependencies are moot.
11
+
12
+ - **Built from:** [askEventDocReferences](../event-doc/ask-event-doc-references.md) (per doc, to find its outbound links) and [askEventDocTransferProvideCollection](#askeventdoctransferprovidecollection) (to run each visit against the right collection's store). Requires an `EventDocTransferRegistry` — read one with `askEventDocTransferReadRegistry`, or call from a built-in transfer route where it's already resolved.
13
+
14
+ ```typescript
15
+ import { askEventDocManifest, askEventDocTransferReadRegistry } from 'quidproquo-features';
16
+
17
+ export function* previewExport(docs: EventDocDocRef[]) {
18
+ const registry = yield* askEventDocTransferReadRegistry();
19
+
20
+ const items = yield* askEventDocManifest(registry, docs);
21
+
22
+ return items; // EventDocManifestItem[] — every doc that would travel, roots first
23
+ }
24
+ ```
25
+
26
+ ## Signature
27
+
28
+ ```typescript
29
+ function* askEventDocManifest(
30
+ registry: EventDocTransferRegistry,
31
+ starts: EventDocDocRef[],
32
+ ): AskResponse<EventDocManifestItem[]>;
33
+ ```
34
+
35
+ ## Parameters
36
+
37
+ | Parameter | Type | Description |
38
+ | --- | --- | --- |
39
+ | `registry` | `EventDocTransferRegistry` | `{ service, collections }` — which collections a transfer may read, as registered by [defineEventDocTransfer](../../../config/features/event-doc-transfer.md). |
40
+ | `starts` | `EventDocDocRef[]` | The selected root docs (`{ service, type, id }`), depth `0` in the result. |
41
+
42
+ ## Returns
43
+
44
+ `EventDocManifestItem[]` — every doc discovered, in discovery order (roots first, then whatever they reference, breadth-first):
45
+
46
+ ```typescript
47
+ type EventDocManifestItem = EventDocDocRef & {
48
+ code: string;
49
+ name: string;
50
+ depth: number; // 0 for a start doc, shortest link distance otherwise
51
+ deleted: boolean; // reported but never bundled
52
+ };
53
+ ```
54
+
55
+ ## Notes
56
+
57
+ - An unregistered or cross-service reference throws `ErrorTypeEnum.BadRequest` rather than being silently skipped — an incomplete manifest can never masquerade as a complete export.
58
+ - Reversing the result is leaves-first, the order [askEventDocTransferExport](./ask-event-doc-transfer-export.md) writes a bundle in and an import applies it in.
59
+
60
+ ## Related
61
+
62
+ - [askEventDocTransferExport](./ask-event-doc-transfer-export.md) — builds and stages a bundle from the same manifest walk.
63
+ - [defineEventDocTransfer](../../../config/features/event-doc-transfer.md) — mounts `POST /transfer/manifest`, the HTTP entry point for this story.
64
+ - [askEventDocReferences](../event-doc/ask-event-doc-references.md) — the per-doc reference read this walks.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: askEventDocTransferExport
3
+ description: Export one or more docs and everything they reference as one staged bundle, and hand back a download link.
4
+ ---
5
+
6
+ # askEventDocTransferExport
7
+
8
+ Exports one or more docs — and everything they reference — as **one** staged bundle, and returns a short-lived download link plus the manifest it covers (so a UI can show exactly what went in, including any soft-deleted docs that were reported but skipped). Selecting several docs at once produces one bundle: their manifests are merged and deduped, so a stylesheet three templates share travels once, not three times.
9
+
10
+ The manifest comes back in discovery order (the starting docs first); the bundle itself is written in **reverse**, which is leaves-first, so on import whatever a doc references always lands before the doc that points at it.
11
+
12
+ - **Built from:** [askEventDocManifest](./ask-event-doc-manifest.md) (the reference walk) then `askEventDocBundleBuild` (reads each doc's full log + assets into the bundle). Requires an `EventDocTransferRegistry` — read one with `askEventDocTransferReadRegistry`, or call from the built-in `POST /transfer/export` route where it's already resolved.
13
+
14
+ ```typescript
15
+ import { askEventDocTransferExport, askEventDocTransferReadRegistry } from 'quidproquo-features';
16
+
17
+ export function* exportSelection(docs: EventDocDocRef[]) {
18
+ const registry = yield* askEventDocTransferReadRegistry();
19
+
20
+ const result = yield* askEventDocTransferExport(registry, docs);
21
+
22
+ return result; // { downloadUrl, filename, items }
23
+ }
24
+ ```
25
+
26
+ ## Signature
27
+
28
+ ```typescript
29
+ function* askEventDocTransferExport(
30
+ registry: EventDocTransferRegistry,
31
+ starts: EventDocDocRef[],
32
+ ): AskResponse<EventDocTransferExportResult>;
33
+ ```
34
+
35
+ ## Parameters
36
+
37
+ | Parameter | Type | Description |
38
+ | --- | --- | --- |
39
+ | `registry` | `EventDocTransferRegistry` | `{ service, collections }` — which collections a transfer may read, as registered by [defineEventDocTransfer](../../../config/features/event-doc-transfer.md). |
40
+ | `starts` | `EventDocDocRef[]` | The docs the caller selected to export (`{ service, type, id }`). |
41
+
42
+ ## Returns
43
+
44
+ `EventDocTransferExportResult`:
45
+
46
+ ```typescript
47
+ type EventDocTransferExportResult = {
48
+ downloadUrl: string; // short-lived, 15 minutes
49
+ filename: string;
50
+ items: EventDocManifestItem[]; // the full manifest, roots first
51
+ };
52
+ ```
53
+
54
+ ## Errors
55
+
56
+ | Condition | Error |
57
+ | --- | --- |
58
+ | `starts` is empty | `ErrorTypeEnum.BadRequest` ("Nothing selected to export.") |
59
+ | A root doc (not just one of its dependencies) is soft-deleted | `ErrorTypeEnum.BadRequest` ("Doc `{id}` is deleted and cannot be exported.") |
60
+
61
+ A dependency that is deleted at source is reported in the manifest and silently skipped from the bundle; a doc the operator explicitly picked being deleted is treated as a mistake worth stopping on instead.
62
+
63
+ ## Related
64
+
65
+ - [askEventDocManifest](./ask-event-doc-manifest.md) — the reference walk this builds on.
66
+ - [askEventDocBundlePlan](./ask-event-doc-bundle-plan.md) / [askEventDocBundleApply](./ask-event-doc-bundle-apply.md) — the other end: review and apply a bundle produced by this story.
67
+ - [defineEventDocTransfer](../../../config/features/event-doc-transfer.md) — mounts `POST /transfer/export`, the HTTP entry point for this story.
@@ -8,7 +8,7 @@ description: Send an event to a state-machine instance to drive a transition, ru
8
8
  Sends an event to a [state-machine](../../config/xstate/state-machine.md) instance, driving a transition. The runtime rehydrates the instance, evaluates its guards, applies the event, runs any side-effect actions, and persists the new state. This is how a durable workflow moves forward over time.
9
9
 
10
10
  - **Action type:** `StateMachineActionType.SendEvent`
11
- - **On the runtime:** the processor loads the instance from the backing key-value store, runs **every** configured guard story with `(entity, event)` to resolve each guard to a boolean, rehydrates the XState actor from the persisted snapshot, sends the event, and captures the new snapshot. If the event did not change state (and the machine is not done), it fails; otherwise it persists the new snapshot and runs the stories mapped to any actions that fired during the transition, passing `(entity, event)`.
11
+ - **On the runtime:** the processor loads the instance from the backing key-value store, runs **every** configured guard story with `(entity, event)` to resolve each guard to a boolean, and rehydrates the XState actor from the persisted snapshot. It then asks XState whether the current state can take the event (with the resolved guard outcomes); if not, it fails with `BadRequest`. Otherwise it sends the event, persists the new snapshot, and runs the stories mapped to any actions that fired during the transition, passing `(entity, event)`.
12
12
 
13
13
  ```typescript
14
14
  import { askStateMachineSendEvent } from 'quidproquo-xstate';
@@ -44,10 +44,10 @@ function* askStateMachineSendEvent<T>(
44
44
  ### `StateMachineEvent`
45
45
 
46
46
  ```typescript
47
- interface StateMachineEvent {
48
- type: string; // the XState event name (e.g. 'PAY', 'CANCEL')
49
- [key: string]: any; // optional payload available to guards and actions
50
- }
47
+ type StateMachineEvent = {
48
+ type: string; // the XState event name (e.g. 'PAY', 'CANCEL')
49
+ [key: string]: unknown; // optional payload available to guards and actions
50
+ };
51
51
  ```
52
52
 
53
53
  ## Returns
@@ -58,7 +58,7 @@ interface StateMachineEvent {
58
58
 
59
59
  - **Guards run first, and all of them.** Before the event is applied, every guard story configured on the machine is executed with `(entity, event)` and reduced to a boolean, then fed to XState. Keep guard stories side-effect-light.
60
60
  - **Actions run after the transition is persisted.** Each XState action that fires during the transition runs its mapped story with `(entity, event)`; a story error aborts the action with that error.
61
- - **Invalid events fail.** If the event produces no state change and the machine is not in a final state, the action fails with `ErrorTypeEnum.BadRequest` (`Event '<type>' is not valid for current state '<state>'`).
61
+ - **Invalid events fail.** If the current state cannot take the event (no matching transition, its guard resolved false, or the machine has already reached a final state), the action fails with `ErrorTypeEnum.BadRequest` (`Event '<type>' is not valid for current state '<state>'`). Self and internal transitions that keep the same state value are valid and run their actions as normal.
62
62
  - Fails with `ErrorTypeEnum.NotFound` if the machine name is unknown or the instance does not exist. Guard/action story errors and the persistence upsert error propagate with their own error types. Use [askCatch](../core/system/ask-catch.md) to handle these in-story.
63
63
 
64
64
  ## Related
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: defineCryptoKey
3
+ description: Define a crypto key, an application-level encryption key for askCryptoEncrypt / askCryptoDecrypt (a KMS CMK on AWS).
4
+ ---
5
+
6
+ # defineCryptoKey
7
+
8
+ Declares a **crypto key**: a named encryption key stories use through [askCryptoEncrypt](../../actions/core/crypto/ask-crypto-encrypt.md) and [askCryptoDecrypt](../../actions/core/crypto/ask-crypto-decrypt.md). The key material never leaves the provider; your code only ever sees opaque ciphertext blobs.
9
+
10
+ - **On AWS:** provisions a KMS customer managed key with automatic rotation enabled, addressed by a deterministic alias derived from application/module/environment, and grants the service's role use of it (`kms:GenerateDataKey*`, `kms:Decrypt`, `kms:Encrypt`, `kms:DescribeKey`). Rotation needs nothing from the app: old key material stays available for decrypt.
11
+ - **On the dev server:** a local master key is seeded on first use at `.qpq-runtime/<app>/cryptoKeys/<service>.json`, so everything works offline with no AWS credentials.
12
+
13
+ One application-level key is usually enough. Separation between callers comes from the `context` on each encrypt (see [askCryptoEncrypt](../../actions/core/crypto/ask-crypto-encrypt.md)), not from separate keys.
14
+
15
+ ```typescript
16
+ import { defineCryptoKey } from 'quidproquo-core';
17
+
18
+ export default [
19
+ defineCryptoKey('app-crypto-key'),
20
+ ];
21
+ ```
22
+
23
+ ## Signature
24
+
25
+ ```typescript
26
+ function defineCryptoKey(
27
+ keyName: string,
28
+ options?: QPQConfigAdvancedCryptoKeySettings,
29
+ ): CryptoKeyQPQConfigSetting;
30
+ ```
31
+
32
+ ## Parameters
33
+
34
+ ### `keyName`: `string` (required)
35
+
36
+ The name of the key, and its `uniqueKey` within the config. This is the name you pass to the crypto actions.
37
+
38
+ ### `options`: `QPQConfigAdvancedCryptoKeySettings` (optional)
39
+
40
+ | Property | Type | Default | Description |
41
+ | --- | --- | --- | --- |
42
+ | `owner` | `CrossModuleOwner<'cryptoKeyName'>` | – | Declares that the key is owned by **another** module/service, so this service is granted use of it rather than creating its own. `{ module, application, feature, environment, cryptoKeyName }`, all optional; unset parts default to the current service. |
43
+
44
+ ## Notes
45
+
46
+ - Ciphertext blobs are versioned (`qpqcrypto:v1:...`), so the underlying mechanism can evolve without re-encrypting stored data.
47
+ - Dev ciphertext is not readable in prod and vice versa; each environment's key is its own.
48
+
49
+ ## Related
50
+
51
+ - [askCryptoEncrypt](../../actions/core/crypto/ask-crypto-encrypt.md): encrypts with this key.
52
+ - [askCryptoDecrypt](../../actions/core/crypto/ask-crypto-decrypt.md): decrypts with this key.
53
+ - [defineSecret](./secret.md): for platform-level secret values set out-of-band, rather than values your app encrypts itself.
@@ -56,6 +56,7 @@ Zero or more sort keys. The list is significant:
56
56
  | `ttlAttribute` | `string` | – | Name of a record attribute holding a Unix-epoch (seconds) timestamp. DynamoDB automatically deletes records once that time passes. |
57
57
  | `disablePointInTimeRecovery` | `boolean` | `false` | Point-in-time recovery (35-day continuous backups / restore) is on by default; set this to opt out. |
58
58
  | `encryption` | `boolean` | `false` | Enables customer-managed KMS encryption for the table (the KMS key comes from the service's AWS config). When a customer-managed key isn't configured, AWS-managed encryption is used instead; when `false`, DynamoDB's default provider-managed encryption still applies. |
59
+ | `onStream` | `KvsStreamSettings` | – | Turns on change data capture and runs a story for every insert/modify/remove on the store. See [Change data capture (`onStream`)](#change-data-capture-onstream). |
59
60
 
60
61
  ## Keys (`CompositeKvsKey`)
61
62
 
@@ -98,6 +99,56 @@ defineKeyValueStore('orders', 'orderId', [], {
98
99
 
99
100
  On AWS each index becomes a Global Secondary Index whose name is the index's partition-key attribute (`customerId`, `status` above).
100
101
 
102
+ ## Change data capture (`onStream`)
103
+
104
+ ```typescript
105
+ export type KvsStreamSettings = {
106
+ runtime: QpqFunctionRuntime;
107
+ coalesceByPartitionKey?: boolean;
108
+ batchSize?: number;
109
+ maximumBatchingWindowInSeconds?: number;
110
+ };
111
+ ```
112
+
113
+ Declaring `onStream` puts the table into DynamoDB's `NEW_AND_OLD_IMAGES` stream mode and deploys a handler lambda subscribed to it. Records for a given partition key are always delivered to the handler in order; different partition keys may be processed concurrently. A table nothing subscribes to gets no stream at all, since a stream on it would be pure cost.
114
+
115
+ | Property | Type | Default | Description |
116
+ | --- | --- | --- | --- |
117
+ | `runtime` | `QpqFunctionRuntime` | – (required) | The handler story, usually a relative path string in the form `'/path/to/file::exportedFunctionName'`. Invoked once per record with a `KvsStreamRecord<T>` (exported from `quidproquo-core`). |
118
+ | `coalesceByPartitionKey` | `boolean` | `false` | Collapse each delivered batch down to one record per partition key (the latest), instead of invoking the handler once per record. Off by default, since a generic consumer (audit trail, change notifications) needs to see every change. Turn it on for a projection, where the handler re-derives state from source and only needs to know a key changed. |
119
+ | `batchSize` | `number` | `100` | Records per invocation. DynamoDB streams allow up to 1000. |
120
+ | `maximumBatchingWindowInSeconds` | `number` | – | How long to wait accumulating records before invoking, 0–300 seconds. Trades latency for fewer invocations, and gives `coalesceByPartitionKey` more to collapse. |
121
+
122
+ The handler receives a `KvsStreamRecord<T>`:
123
+
124
+ ```typescript
125
+ export enum KvsStreamEventType {
126
+ Insert = 'Insert',
127
+ Modify = 'Modify',
128
+ Remove = 'Remove',
129
+ }
130
+
131
+ export type KvsStreamRecord<TItem extends object = any> = {
132
+ keyValueStoreName: string;
133
+ eventType: KvsStreamEventType;
134
+ scope?: string; // present when the item was written under a storage scope
135
+ keys: Record<string, unknown>; // always present, including on Remove
136
+ newImage?: TItem; // absent on Remove
137
+ oldImage?: TItem; // absent on Insert
138
+ };
139
+ ```
140
+
141
+ Images are plain objects, already unmarshalled from DynamoDB's wire format and with any storage scope stripped back out of the key values — a handler is ordinary story code and never sees a raw AttributeValue or a composed partition key.
142
+
143
+ ```typescript
144
+ defineKeyValueStore('documents', 'id', [], {
145
+ onStream: {
146
+ runtime: '/entry/kvsStream/onDocumentChanged::onDocumentChanged',
147
+ coalesceByPartitionKey: true,
148
+ },
149
+ });
150
+ ```
151
+
101
152
  ## Examples
102
153
 
103
154
  ```typescript
@@ -132,4 +183,5 @@ export default [
132
183
  - **Query & scan:** [askKeyValueStoreQuery](../../actions/core/key-value-store/ask-key-value-store-query.md) · [askKeyValueStoreScan](../../actions/core/key-value-store/ask-key-value-store-scan.md)
133
184
  - **Write:** [askKeyValueStoreUpsert](../../actions/core/key-value-store/ask-key-value-store-upsert.md) · [askKeyValueStoreUpdate](../../actions/core/key-value-store/ask-key-value-store-update.md) · [askKeyValueStoreDelete](../../actions/core/key-value-store/ask-key-value-store-delete.md)
134
185
  - **AWS tuning:** [defineAwsKmsKey](../config-aws/aws-kms-key.md) (customer-managed encryption key for the `encryption` flag), [defineAwsDyanmoOverrideForKvs](../config-aws/aws-dyanmo-override-for-kvs.md) (back the store with a pre-existing DynamoDB table), and [defineAwsDataStoreRemovalPolicy](../config-aws/aws-data-store-removal-policy.md) (retain vs destroy the table on teardown).
135
- - **AWS implementation:** `QpqCoreKeyValueStoreConstruct` (DynamoDB table, LSIs, GSIs, TTL, PITR, KMS, IAM grants) in `quidproquo-deploy-awscdk`; KVS action processors in `quidproquo-actionprocessor-awslambda`.
186
+ - [defineEventDocSummary](../features/event-doc-summary.md) — a real `onStream` consumer: rebuilds a document's summary record from its event log on every change.
187
+ - **AWS implementation:** `QpqCoreKeyValueStoreConstruct` (DynamoDB table, LSIs, GSIs, TTL, PITR, KMS, IAM grants) and `QpqApiCoreKeyValueStoreStreamConstruct` (the `onStream` handler lambda + event source) in `quidproquo-deploy-awscdk`; KVS action processors in `quidproquo-actionprocessor-awslambda`.