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
@@ -0,0 +1,7 @@
1
+ import { CreateQpqAppAnswers } from './CreateQpqAppAnswers';
2
+ export type StepContext = {
3
+ targetDirectory: string;
4
+ templateDirectory: string;
5
+ ownVersion: string;
6
+ answers: CreateQpqAppAnswers;
7
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=StepContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StepContext.js","sourceRoot":"","sources":["../../../src/types/StepContext.ts"],"names":[],"mappings":"","sourcesContent":["import { CreateQpqAppAnswers } from './CreateQpqAppAnswers';\n\nexport type StepContext = {\n // Absolute path of the directory being scaffolded (<cwd>/<appName>).\n targetDirectory: string;\n // Absolute path of the bundled template snapshot (a pruned quidproquojs.com).\n templateDirectory: string;\n // create-qpq-app's own version. The generated app pins its quidproquo-*\n // dependencies to this (the packages are published in lockstep).\n ownVersion: string;\n answers: CreateQpqAppAnswers;\n};\n"]}
@@ -0,0 +1,4 @@
1
+ export * from './AppLanguage';
2
+ export * from './CreateQpqAppAnswers';
3
+ export * from './CreateQpqAppStep';
4
+ export * from './StepContext';
@@ -0,0 +1,5 @@
1
+ export * from './AppLanguage';
2
+ export * from './CreateQpqAppAnswers';
3
+ export * from './CreateQpqAppStep';
4
+ export * from './StepContext';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC","sourcesContent":["export * from './AppLanguage';\nexport * from './CreateQpqAppAnswers';\nexport * from './CreateQpqAppStep';\nexport * from './StepContext';\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.11",
4
- "description": "Scaffold a new quidproquo app — npx create-qpq-app my-app",
3
+ "version": "0.1.13",
4
+ "description": "Scaffold a new quidproquo app: npx create-qpq-app my-app",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/esm/index.js",
7
7
  "types": "./lib/commonjs/index.d.ts",
@@ -14,7 +14,8 @@
14
14
  "template"
15
15
  ],
16
16
  "scripts": {
17
- "test": "echo \"Error: no test specified\" && exit 1",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
18
19
  "clean": "npx rimraf lib && npx rimraf template && npx rimraf node_modules",
19
20
  "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:bin-perms",
20
21
  "watch": "tsc -p tsconfig.commonjs.json -w",
@@ -51,7 +52,7 @@
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "^22.13.13",
54
- "quidproquo-tsconfig": "0.1.11"
55
+ "quidproquo-tsconfig": "0.1.13"
55
56
  },
56
57
  "bin": {
57
58
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -0,0 +1,7 @@
1
+ {
2
+ "label": "Crypto",
3
+ "link": {
4
+ "type": "generated-index",
5
+ "description": "Crypto actions encrypt and decrypt values with a key declared by defineCryptoKey. Envelope encryption with an optional context that binds ciphertext to the place it was created."
6
+ }
7
+ }
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: askCryptoDecrypt
3
+ description: Decrypt a ciphertext produced by askCryptoEncrypt, verifying its context.
4
+ ---
5
+
6
+ # askCryptoDecrypt
7
+
8
+ Decrypts a blob produced by [askCryptoEncrypt](./ask-crypto-encrypt.md) and returns the original string. The `context` supplied here must match the one supplied at encrypt time exactly, or the call fails before any plaintext is produced.
9
+
10
+ - **Action type:** `CryptoActionType.Decrypt`
11
+ - **On AWS:** unwraps the embedded data key with AWS KMS (`kms:Decrypt`, with the context as KMS encryption context, which also gives a per-context audit trail in CloudTrail) and decrypts locally with AES-256-GCM.
12
+ - **On the dev server:** identical code path against the local master key, including identical context enforcement. A context scoping bug fails the same way locally as in prod.
13
+
14
+ ```typescript
15
+ import { askCryptoDecrypt, askKeyValueStoreGet } from 'quidproquo-core';
16
+
17
+ export function* askReadCustomerApiKey(customerId: string) {
18
+ const record = yield* askKeyValueStoreGet('customer-credentials', customerId);
19
+
20
+ return yield* askCryptoDecrypt('app-crypto-key', record.apiKey, { customerId });
21
+ }
22
+ ```
23
+
24
+ ## Signature
25
+
26
+ ```typescript
27
+ function* askCryptoDecrypt(
28
+ keyName: string,
29
+ ciphertext: string,
30
+ context?: CryptoContext,
31
+ ): AskResponse<string>;
32
+ ```
33
+
34
+ ## Parameters
35
+
36
+ | Parameter | Type | Description |
37
+ | --- | --- | --- |
38
+ | `keyName` | `string` | Name of the crypto key, declared with [defineCryptoKey](../../../config/core/crypto-key.md) (or shared via its `owner` option). |
39
+ | `ciphertext` | `string` | A blob previously returned by [askCryptoEncrypt](./ask-crypto-encrypt.md). |
40
+ | `context` | `CryptoContext` | The same `Record<string, string>` supplied at encrypt time. Omitting it and passing `{}` are equivalent. |
41
+
42
+ ## Returns
43
+
44
+ `string`: the original plaintext.
45
+
46
+ ## Errors
47
+
48
+ Each failure mode is distinguishable because they need different handling:
49
+
50
+ | Error | Meaning |
51
+ | --- | --- |
52
+ | `CryptoDecryptErrorTypeEnum.ContextMismatch` | The supplied context differs from the encrypt-time context. Probable scoping bug in the caller; alert, do not retry. |
53
+ | `CryptoDecryptErrorTypeEnum.MalformedCiphertext` | The blob is corrupt, truncated, or not a qpq crypto blob. Data problem; the stored value needs re-creating. |
54
+ | `CryptoDecryptErrorTypeEnum.KeyNotConfigured` | No `defineCryptoKey` with that name exists in the service config. |
55
+ | `CryptoDecryptErrorTypeEnum.KeyUnavailable` | The key is disabled, deleted, or access was denied. Infrastructure problem; surface to ops. |
56
+ | `CryptoDecryptErrorTypeEnum.Throttling` | The provider rate limit was exceeded; back off and retry. |
57
+
58
+ ## Related
59
+
60
+ - [askCryptoEncrypt](./ask-crypto-encrypt.md): produces the ciphertext, and documents how `context` works.
61
+ - [defineCryptoKey](../../../config/core/crypto-key.md): declares the key this action uses.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: askCryptoEncrypt
3
+ description: Encrypt a value with a configured crypto key, optionally bound to a context.
4
+ ---
5
+
6
+ # askCryptoEncrypt
7
+
8
+ Encrypts a string with a [crypto key](../../../config/core/crypto-key.md) and returns an opaque, versioned ciphertext blob. Store the blob anywhere (a key value store, an event doc); only [askCryptoDecrypt](./ask-crypto-decrypt.md) can read it back.
9
+
10
+ - **Action type:** `CryptoActionType.Encrypt`
11
+ - **On AWS:** envelope encryption backed by AWS KMS. A data key is generated under the CMK (`kms:GenerateDataKey`) and the value is encrypted locally with AES-256-GCM, so there is no practical size limit and most calls never leave the process (data keys are cached briefly). The key itself is provisioned by [defineCryptoKey](../../../config/core/crypto-key.md).
12
+ - **On the dev server:** the same envelope code runs against a local master key seeded at `.qpq-runtime/<app>/cryptoKeys/<service>.json`. No AWS credentials or network needed. Dev ciphertext is not readable in prod (or on another machine), and that is intentional.
13
+
14
+ ```typescript
15
+ import { askCryptoEncrypt, askKeyValueStoreUpdate } from 'quidproquo-core';
16
+
17
+ export function* askStoreCustomerApiKey(customerId: string, apiKey: string) {
18
+ const ciphertext = yield* askCryptoEncrypt('app-crypto-key', apiKey, { customerId });
19
+
20
+ yield* askKeyValueStoreUpdate('customer-credentials', { customerId, apiKey: ciphertext });
21
+ }
22
+ ```
23
+
24
+ ## Context
25
+
26
+ `context` is an optional `Record<string, string>` mixed into the encryption as additional authenticated data. The crypto layer treats it as opaque: it does not know or care what the keys mean. Supplying it buys two things:
27
+
28
+ - **Binding.** The ciphertext is only valid under the same context. A blob encrypted with `{ customerId: 'a' }` cannot be decrypted with `{ customerId: 'b' }`, so copying it to another row makes it undecryptable rather than readable.
29
+ - **Non-forgeability.** The association lives inside the authentication tag, not in an editable field beside the ciphertext. Forging it would require the key.
30
+
31
+ Context values are **not encrypted and not secret**. On AWS they are sent to KMS as encryption context and appear in CloudTrail in the clear, so never put sensitive values in them. Omitting `context` and passing `{}` are equivalent.
32
+
33
+ ## Signature
34
+
35
+ ```typescript
36
+ function* askCryptoEncrypt(
37
+ keyName: string,
38
+ plaintext: string,
39
+ context?: CryptoContext,
40
+ ): AskResponse<string>;
41
+ ```
42
+
43
+ ## Parameters
44
+
45
+ | Parameter | Type | Description |
46
+ | --- | --- | --- |
47
+ | `keyName` | `string` | Name of the crypto key, declared with [defineCryptoKey](../../../config/core/crypto-key.md) (or shared via its `owner` option). |
48
+ | `plaintext` | `string` | The value to encrypt. |
49
+ | `context` | `CryptoContext` | Optional `Record<string, string>` bound into the ciphertext; the same values must be supplied at decrypt. |
50
+
51
+ ## Returns
52
+
53
+ `string`: an opaque, versioned ciphertext blob (`qpqcrypto:v1:...`). Treat it as a black box; its internal format can change between versions without a migration.
54
+
55
+ ## Errors
56
+
57
+ | Error | Meaning |
58
+ | --- | --- |
59
+ | `CryptoEncryptErrorTypeEnum.KeyNotConfigured` | No `defineCryptoKey` with that name exists in the service config. |
60
+ | `CryptoEncryptErrorTypeEnum.KeyUnavailable` | The key exists in config but is disabled, deleted, or access was denied. Infrastructure problem; surface to ops. |
61
+ | `CryptoEncryptErrorTypeEnum.Throttling` | The provider rate limit was exceeded; back off and retry. |
62
+
63
+ ## Related
64
+
65
+ - [defineCryptoKey](../../../config/core/crypto-key.md): declares the key this action uses.
66
+ - [askCryptoDecrypt](./ask-crypto-decrypt.md): reads the value back.
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: askKeyValueStoreScanAllScopes
3
+ description: Migration-only scan across every scope in a key-value store, each record tagged with the scope it came from.
4
+ ---
5
+
6
+ # askKeyValueStoreScanAllScopes
7
+
8
+ Reads a page of records from a [key-value store](../../../config/core/key-value-store.md) across **every** storage scope, plus the unscoped partition, pairing each record with the scope it lives under. This deliberately crosses the scope boundary that [askKeyValueStoreScan](./ask-key-value-store-scan.md) and the rest of the KVS layer hold: an ordinary scan excludes scope-composed records so one tenant's request can never read another's.
9
+
10
+ Reach for this only from a migration or an equivalent whole-store operation that needs to rewrite every row regardless of scope — never on a request path. Nothing in the framework calls it.
11
+
12
+ - **Action type:** `KeyValueStoreActionType.ScanAllScopes`
13
+
14
+ ```typescript
15
+ import { askKeyValueStoreScanAllScopes } from 'quidproquo-core';
16
+
17
+ interface User {
18
+ userId: string;
19
+ status: string;
20
+ }
21
+
22
+ export function* askMigrateAllUsers() {
23
+ let nextPageKey: string | undefined;
24
+
25
+ do {
26
+ const page = yield* askKeyValueStoreScanAllScopes<User>('users', undefined, nextPageKey);
27
+
28
+ for (const { scope, item } of page.items) {
29
+ // rewrite item back into the same scope it came from
30
+ }
31
+
32
+ nextPageKey = page.nextPageKey;
33
+ } while (nextPageKey);
34
+ }
35
+ ```
36
+
37
+ ## Signature
38
+
39
+ ```typescript
40
+ function* askKeyValueStoreScanAllScopes<KvsItem>(
41
+ keyValueStoreName: string,
42
+ filterCondition?: KvsQueryOperation,
43
+ nextPageKey?: string,
44
+ options?: KeyValueStoreScanAllScopesOptions,
45
+ ): AskResponse<QpqPagedData<KvsScopedItem<KvsItem>>>;
46
+ ```
47
+
48
+ ## Parameters
49
+
50
+ | Parameter | Type | Description |
51
+ | --- | --- | --- |
52
+ | `keyValueStoreName` | `string` | Name of the store to scan — must match a store declared with [defineKeyValueStore](../../../config/core/key-value-store.md) (or one shared via its `owner` option). |
53
+ | `filterCondition` | `KvsQueryOperation` | Optional filter applied to every scanned record, within every scope. Built with the `kvs*` condition helpers — see [Query conditions](./ask-key-value-store-query.md#query-conditions-kvsqueryoperation). Omit it to return everything. |
54
+ | `nextPageKey` | `string` | Opaque cursor from a previous page's `nextPageKey`; pass it to fetch the following page. |
55
+ | `options` | `KeyValueStoreScanAllScopesOptions` | Optional scan options (see below). |
56
+
57
+ ### `KeyValueStoreScanAllScopesOptions`
58
+
59
+ | Property | Type | Default | Description |
60
+ | --- | --- | --- | --- |
61
+ | `limit` | `number` | – | Accepted but not implemented: no processor currently caps the page size, so setting it has no effect. |
62
+
63
+ ## Returns
64
+
65
+ `QpqPagedData<KvsScopedItem<KvsItem>>` — one page of results:
66
+
67
+ ```typescript
68
+ interface KvsScopedItem<KvsItem> {
69
+ scope?: string; // absent for an unscoped record
70
+ item: KvsItem;
71
+ }
72
+
73
+ interface QpqPagedData<T> {
74
+ items: T[];
75
+ nextPageKey?: string; // present when more pages remain
76
+ }
77
+ ```
78
+
79
+ ## Errors
80
+
81
+ | Error | Meaning |
82
+ | --- | --- |
83
+ | `KeyValueStoreScanAllScopesErrorTypeEnum.ServiceUnavailable` | DynamoDB internal error or throttling. |
84
+ | `KeyValueStoreScanAllScopesErrorTypeEnum.ResourceNotFound` | The underlying table does not exist. |
85
+ | `KeyValueStoreScanAllScopesErrorTypeEnum.StoreNotFound` | The key value store is not declared in the qpq config (misconfiguration, e.g. a wrong name or a missing `defineKeyValueStore`). |
86
+
87
+ Catch errors with `askCatch` — it returns `{ success: true, result }` or `{ success: false, error }`.
88
+
89
+ ## Notes
90
+
91
+ - Drain every page: pagination can advance across scope boundaries as well as within a single scope's records, so stopping after the first page silently skips the rest of the store, not just the rest of one scope.
92
+ - The scope is reported rather than dropped because a caller rewriting records across every tenant in one pass has to know which tenant each belongs to, or a write lands in the wrong partition.
93
+
94
+ ## Related
95
+
96
+ - [defineKeyValueStore](../../../config/core/key-value-store.md) — declares the store being scanned.
97
+ - [askKeyValueStoreScan](./ask-key-value-store-scan.md) — the scoped-safe equivalent for request-path code.
98
+ - [askKeyValueStoreScanAll](./ask-key-value-store-scan-all.md) — drains all pages of a single-scope scan into one array.
@@ -1 +1 @@
1
- { "label": "quidproquo-features", "position": 4, "link": { "type": "generated-index", "description": "Action requesters from quidproquo-features — event-document, AI-chat, admin, and validation stories." } }
1
+ { "label": "quidproquo-features", "position": 4, "link": { "type": "generated-index", "description": "Action requesters from quidproquo-features — event-document, event-document transfer, AI-chat, admin, and validation stories." } }
@@ -50,11 +50,11 @@ function* askEventDocAppendServerEvent<T>(
50
50
 
51
51
  ## Returns
52
52
 
53
- `AskResponse<EventDocEvent>` — the appended event with its server-stamped metadata (`index`, `createdAt`, `createdBy`, and the generated `clientMessageId`).
53
+ `AskResponse<EventDocEvent>` — the appended event with its server-stamped metadata (`eventId`, `createdAt`, `createdBy`, and the generated `clientMessageId`).
54
54
 
55
55
  ## Notes
56
56
 
57
- - All of [askEventDocEventAppend](./ask-event-doc-event-append.md)'s invariants apply — version monotonicity, lifecycle/payload validation, and optimistic-concurrency retry — since this is a thin envelope-building wrapper over it. It can therefore throw the same `ErrorTypeEnum.NotFound` / `ErrorTypeEnum.Conflict`.
57
+ - This is a thin envelope-building wrapper over [askEventDocEventAppend](./ask-event-doc-event-append.md), so the same caveat applies: the event is written unconditionally, and version/lifecycle validation is decided later at fold time, not here. A server event that a validator would reject is written but silently skipped by every fold.
58
58
  - A fresh `clientMessageId` is generated on every call, so this path does not participate in client retry dedup — each call is a distinct intended event.
59
59
 
60
60
  ## Related
@@ -64,7 +64,7 @@ A point-in-time snapshot of who produced an event, captured server-side at appen
64
64
 
65
65
  ## askEventDocSeedInitState
66
66
 
67
- Seeds a new document's log with its `INIT_STATE` event at index `0`, carrying the identity (`id`/`code`/`name`). This is the create-only primitive `askEventDocCreate` composes; clients never send `INIT_STATE` themselves. It writes the event and returns it.
67
+ Seeds a new document's log with its `INIT_STATE` event, carrying the identity (`id`/`code`/`name`). This is the create-only primitive `askEventDocCreate` composes; clients never send `INIT_STATE` themselves. It writes the event and returns it.
68
68
 
69
69
  ```typescript
70
70
  function* askEventDocSeedInitState(
@@ -82,7 +82,7 @@ function* askEventDocSeedInitState(
82
82
  | `name` | `string` | The document's name. |
83
83
  | `actor` | `EventDocEventActor` | Who is creating the document. |
84
84
 
85
- **Returns** `EventDocEvent` — the written `INIT_STATE` event, with `payload.metadata.index === 0` and `version === 1`.
85
+ **Returns** `EventDocEvent` — the written `INIT_STATE` event, with a freshly minted sortable `payload.metadata.eventId` and `version === 1`.
86
86
 
87
87
  Use `askEventDocCreate` unless you are building a custom create flow that needs the raw event; `askEventDocSeedInitState` alone writes the log but does **not** derive or persist the summary record.
88
88
 
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  title: askEventDocEventAppend
3
- description: Append a client-authored event to a document's log with dedup, version and lifecycle validation, and optimistic-concurrency retry.
3
+ description: Append a client-authored event to a document's log — a single unconditional write with no read, no retry, and no validation.
4
4
  ---
5
5
 
6
6
  # askEventDocEventAppend
7
7
 
8
- Appends a single client-authored event to a document's ordered event stream — the write half of the event-sourcing core. This is where the append-time safety invariants live: idempotent dedup, version monotonicity, lifecycle/payload validation, and optimistic-concurrency retry. After the event is written it also re-derives the queryable summary record so the document's status, version, name, and timestamps stay in sync with the log.
8
+ Appends a single client-authored event to a document's ordered event stream — the write half of the event-sourcing core. The event's id is a sortable id (UUIDv7, minted by [askNewSortableGuid](../../core/guid/ask-new-sortable-guid.md)), so the write needs no allocator and no coordination: it does not read the tail, does not validate, and has no retry loop. Concurrent appends to the same document neither contend nor fail on each other. After the event is written it also re-derives the queryable summary record so the document's status, version, name, and timestamps stay in sync with the log.
9
9
 
10
- - **Built from:** a story composing `askRetry`, `askEventDocEventLast`, `askEventDocEventListAll`, `askEventDocEventWrite`, `askEventDocGetByIdOrThrow`, and (when the collection configures one) an `askInlineFunctionExecute` validator. Not a single action.
10
+ **Validation happens later, at fold time, not here.** Dedup (a repeated `clientMessageId`), version monotonicity, and lifecycle/domain rules are all decided when the log is folded, against the accepted events before the one in question. An event that fails one of those checks is not rejected at append — it is written, then silently skipped by every fold, so the document reads as though it was never sent. That silence is deliberate: clients are expected to validate before they send (the same rules run client-side against the pending buffer), so a skipped event means a client skipped its own pre-flight, not that the append needs to report an error.
11
+
12
+ - **Built from:** `askDateNow`, `askNewSortableGuid`, `askEventDocEventWrite`, and `askEventDocSummaryRederive` (plus, when the collection configures `onPublish`/`onAppend`, `askEventDocGetByIdOrThrow`, `askEventDocEventListAll`, and `askInlineFunctionExecute`). Not a single action.
11
13
  - **Requires the store context** — wrap the call in [askEventDocProvideStore](./ask-event-doc-provide-store.md) (custom routes) or [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals) (built-in routes).
12
14
 
13
15
  ```typescript
@@ -28,7 +30,7 @@ export function* appendTitleChange(docId: string) {
28
30
  actor,
29
31
  );
30
32
 
31
- return event.payload.metadata.index;
33
+ return event.payload.metadata.eventId;
32
34
  }
33
35
  ```
34
36
 
@@ -46,20 +48,20 @@ function* askEventDocEventAppend(
46
48
 
47
49
  | Parameter | Type | Description |
48
50
  | --- | --- | --- |
49
- | `modelId` | `string` | The document id whose log the event is appended to. The document must already have an `INIT_STATE` event (created via `askEventDocCreate`), or the append throws `NotFound`. |
51
+ | `modelId` | `string` | The document id whose log the event is appended to. Not checked against an existing `INIT_STATE` at append time — an event appended before `INIT_STATE` exists is simply written and then skipped by every fold, since the reducer has no document to fold it onto. |
50
52
  | `input` | `EventDocEventInput` | The client-authored event envelope — see below. |
51
53
  | `actor` | `EventDocEventActor` | Who authored the event; stamped onto the event as `createdBy`. Usually obtained from [askEventDocResolveActor](./ask-event-doc-resolve-actor.md). |
52
54
 
53
55
  ### `EventDocEventInput`
54
56
 
55
- What the client POSTs to append an event. `modelId` and the server-stamped provenance (`index`, `createdAt`, `createdBy`) are NOT part of it.
57
+ What the client POSTs to append an event. `modelId` and the server-stamped provenance (`eventId`, `createdAt`, `createdBy`) are NOT part of it.
56
58
 
57
59
  | Property | Type | Description |
58
60
  | --- | --- | --- |
59
61
  | `type` | `string` | The effect/event type discriminant (e.g. `SET_NAME`). The reducer folds it by this. |
60
62
  | `payload.data` | `T` | The typed domain data for the event. |
61
- | `payload.metadata.version` | `number` | The schema version the client authored against. Must be `>=` the last event's version — an older version throws `Conflict`. |
62
- | `payload.metadata.clientMessageId` | `string` | A client-generated id used for dedup: if the latest event already carries it, the append is a no-op and returns that event unchanged. |
63
+ | `payload.metadata.version` | `number` | The schema version the client authored against. Expected `>=` the log's highest accepted version so far — the fold, not the append, silently skips an older one when it later folds the log. |
64
+ | `payload.metadata.clientMessageId` | `string` | A client-generated id used for dedup: the fold ignores a later event carrying a `clientMessageId` it has already accepted. The append itself does not check this — a retry is written as a new row in the log either way. |
63
65
 
64
66
  ### `EventDocEventActor`
65
67
 
@@ -70,7 +72,7 @@ What the client POSTs to append an event. `modelId` and the server-stamped prove
70
72
 
71
73
  ## Returns
72
74
 
73
- `AskResponse<EventDocEvent>` — the event that now lives in the log, with server-stamped metadata (`index`, `createdAt`, `createdBy`) filled in. On a deduped retry, the pre-existing event is returned unchanged.
75
+ `AskResponse<EventDocEvent>` — the event now written to the log, with server-stamped metadata (`eventId`, `createdAt`, `createdBy`) filled in. Unlike before, this is not conditional on the event surviving validation — a fold may still skip it.
74
76
 
75
77
  ### `EventDocEvent`
76
78
 
@@ -78,15 +80,15 @@ What the client POSTs to append an event. `modelId` and the server-stamped prove
78
80
  | --- | --- | --- |
79
81
  | `type` | `string` | The event type discriminant. |
80
82
  | `payload.data` | `T` | The typed domain data. |
81
- | `payload.metadata` | `EventDocEventMetadata` | Full provenance: `version`, `clientMessageId`, `createdBy`, `createdAt`, and `index` (mirrors the storage sort key). |
83
+ | `payload.metadata` | `EventDocEventMetadata` | Full provenance: `version`, `clientMessageId`, `createdBy`, `createdAt`, and `eventId` (a sortable id — mirrors the storage sort key, sorts lexicographically in creation order). |
82
84
 
83
85
  ## Notes
84
86
 
85
- - **Dedup** is best-effort against the latest event only (until a GSI exists): a retry that re-sends the same `clientMessageId` returns the existing tail event without writing.
86
- - **Validation** always runs against the log folded from prior events. If the collection configured an `eventValidator` inline function it runs that (a complete validator that already composes the reserved lifecycle guard); otherwise it runs `defaultEventDocEventValidator` — the same guard with no domain rules. Exactly one validator runs. A rejected event throws `Conflict` with the validator's reason.
87
- - **Concurrency:** the underlying write ([askEventDocEventWrite](./ask-event-doc-event-write.md)) claims the `(modelId, index)` slot conditionally. A losing concurrent writer gets a key-value-store upsert conflict — the only error the internal `askRetry` re-laps on — re-reads the tail, and re-runs dedup/validation against fresh state, so concurrent appends serialize onto consecutive indexes. After `MAX_APPEND_ATTEMPTS` (8) lost races it throws `ErrorTypeEnum.Conflict`.
88
- - **Thrown `ErrorTypeEnum` values:** `NotFound` (no `INIT_STATE`), `Conflict` (stale version, failed validation, or exhausted concurrency retries). These are thrown via `askThrowError`, not a per-action error enum.
89
- - After writing, it re-derives and upserts the document's summary record (via `applyEventDocSummaryEvent`) so the queryable view stays consistent with the log.
87
+ - **No dedup, no version check, and no lifecycle/domain validation at append time.** All three are decided when the log is folded (`foldEventDocLog`), against the accepted events before the one in question: a repeated `clientMessageId` is ignored, an event whose version is older than the log's highest accepted version is ignored, and the collection's `eventValidator` (or `defaultEventDocEventValidator` when none is configured) is run there too. A rejected event is skipped silently — the document reads as though it was never written — rather than causing the append to throw.
88
+ - **No read, no retry, no coordination.** The append does not read the tail or the log; it mints a sortable id and writes. Two appends landing in the same millisecond get an arbitrary but stable relative order, which is fine because ordering only has to be stable, not wall-clock-precise.
89
+ - **Write uniqueness** is still enforced by [askEventDocEventWrite](./ask-event-doc-event-write.md)'s conditional (`ifNotExists`) write, but since ids are unique by construction this should never fire in practice — a collision surfaces as `KeyValueStoreUpsertErrorTypeEnum.Conflict` and indicates a bug (two writers minting the same id), not ordinary contention, so there is no retry around it.
90
+ - After writing, it calls `askEventDocSummaryRederive`, which re-folds the whole log and re-derives the document's summary record so the queryable view (identity, version history, timestamps) stays in sync — this is the one piece of read-model maintenance still on the write path, until a stream projector replaces it.
91
+ - Hooks (`onPublish`/`onAppend`, when the collection configures them) run after the event is durably written; a hook failure propagates so the caller knows the side effect — not the append — failed.
90
92
 
91
93
  ## Related
92
94
 
@@ -5,21 +5,21 @@ description: Read a document's event log — a page of events, the whole log fla
5
5
 
6
6
  # Reading the event log
7
7
 
8
- Three read helpers over a document's event stream. All three resolve the collection's events store from the store context and query it by `pk = modelId`, ascending by log index (except `askEventDocEventLast`, which reads the tail). They are the read side of the event-sourcing core, feeding the fold that reconstructs a document from its events.
8
+ Three read helpers over a document's event stream. All three resolve the collection's events store from the store context and query it by `pk = modelId`, ascending by event id (except `askEventDocEventLast`, which reads the tail). Event ids are sortable ids (UUIDv7) whose string form sorts lexicographically in creation order, so ascending-by-id and ascending-by-creation-time agree. They are the read side of the event-sourcing core, feeding the fold that reconstructs a document from its events.
9
9
 
10
10
  - **Requires the store context** — provide it via [askEventDocProvideStore](./ask-event-doc-provide-store.md) / [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals).
11
11
  - **Built from:** [askKeyValueStoreQuery](../../core/key-value-store/ask-key-value-store-query.md) against the events store, plus [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore). Not single actions.
12
12
 
13
13
  ## askEventDocEventList
14
14
 
15
- Returns one page of events for a document, oldest first. Supports paging and, via `afterIndex`, fetching only the tail since a known index — an incremental refresh.
15
+ Returns one page of events for a document, oldest first. Supports paging and, via `afterEventId`, fetching only the tail since a known event — an incremental refresh.
16
16
 
17
17
  ```typescript
18
18
  import { askEventDocEventList } from 'quidproquo-features';
19
19
 
20
- export function* refreshSince(docId: string, lastSeenIndex: number) {
21
- const page = yield* askEventDocEventList(docId, { afterIndex: lastSeenIndex });
22
- return page.items; // events with index > lastSeenIndex
20
+ export function* refreshSince(docId: string, lastSeenEventId: string) {
21
+ const page = yield* askEventDocEventList(docId, { afterEventId: lastSeenEventId });
22
+ return page.items; // events after lastSeenEventId
23
23
  }
24
24
  ```
25
25
 
@@ -45,11 +45,11 @@ function* askEventDocEventList(
45
45
  | --- | --- | --- | --- |
46
46
  | `limit` | `number` | (store default) | Max number of events to return in the page. |
47
47
  | `nextPageKey` | `string` | — | Continuation token from a previous page's `nextPageKey`. |
48
- | `afterIndex` | `number` | — | Return only events whose log index is greater than this (exclusive). A sort-key range condition on the events store's primary key — no GSI involved. |
48
+ | `afterEventId` | `string` | — | Return only events whose event id sorts after this one (exclusive). A sort-key range condition on the events store's primary key — no GSI involved. |
49
49
 
50
50
  ### Returns
51
51
 
52
- `AskResponse<QpqPagedData<EventDocEvent>>` — `{ items: EventDocEvent[]; nextPageKey?: string }`. Events are ordered ascending by index; `nextPageKey` is present when more events remain.
52
+ `AskResponse<QpqPagedData<EventDocEvent>>` — `{ items: EventDocEvent[]; nextPageKey?: string }`. Events are ordered ascending by event id (equivalently, creation order); `nextPageKey` is present when more events remain.
53
53
 
54
54
  ## askEventDocEventListAll
55
55
 
@@ -77,11 +77,11 @@ function* askEventDocEventListAll(modelId: string): AskResponse<EventDocEvent[]>
77
77
 
78
78
  ### Returns
79
79
 
80
- `AskResponse<EventDocEvent[]>` — every event for the document, ordered ascending by index. Internally loops [askEventDocEventList](#askeventdoceventlist) until there is no `nextPageKey`.
80
+ `AskResponse<EventDocEvent[]>` — every event for the document, ordered ascending by event id. Internally loops [askEventDocEventList](#askeventdoceventlist) until there is no `nextPageKey`.
81
81
 
82
82
  ## askEventDocEventLast
83
83
 
84
- Returns the newest event in the log, or `null` if the document has no events. Used to assign the next index, dedup, and validate during an append. Relies on numeric sort-key ordering (the dev server sorts numeric sort keys numerically, matching DynamoDB), so it returns the true latest.
84
+ Returns the newest event in the log, or `null` if the document has no events. Relies on string sort-key ordering (a sortable event id's string form sorts lexicographically in creation order, and the dev server sorts string sort keys the same way DynamoDB does), so it returns the true latest.
85
85
 
86
86
  ```typescript
87
87
  import { askEventDocEventLast } from 'quidproquo-features';
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: askEventDocEventWrite
3
- description: Low-level conditional write of a single event to a document's events store, claiming its (modelId, index) slot atomically.
3
+ description: Low-level conditional write of a single event to a document's events store, keyed by its sortable event id.
4
4
  ---
5
5
 
6
6
  # askEventDocEventWrite
7
7
 
8
- The low-level write primitive behind the event log. It persists one already-built [EventDocEvent](./ask-event-doc-event-append.md#eventdocevent) into the collection's events store, keyed by `pk = modelId` / `sk = index`. The write is **conditional** (`ifNotExists`): the `(modelId, index)` slot is claimed atomically, so a concurrent writer that computed the same index gets a conflict instead of silently overwriting the event.
8
+ The low-level write primitive behind the event log. It persists one already-built [EventDocEvent](./ask-event-doc-event-append.md#eventdocevent) into the collection's events store, keyed by `pk = modelId` / `sk = eventId` (a sortable id — UUIDv7 — whose string form sorts lexicographically in creation order). The write is **conditional** (`ifNotExists`), but since ids are minted uniquely (via `askNewSortableGuid`) rather than allocated, this is a cheap uniqueness assertion rather than a contested slot: a `Conflict` here means two writers minted the same id, which should never happen and indicates a bug, not ordinary concurrent-write contention.
9
9
 
10
- Ordering, index assignment, dedup, validation, and the conflict-retry loop all live one layer up in [askEventDocEventAppend](./ask-event-doc-event-append.md) — you almost always want that instead. Call this directly only when you are implementing your own append semantics.
10
+ Id assignment, dedup, and validation all live one layer up in [askEventDocEventAppend](./ask-event-doc-event-append.md) (dedup and validation are actually decided later still, at fold time) — you almost always want that instead. Call this directly only when you are implementing your own append semantics.
11
11
 
12
12
  - **Built from:** [askKeyValueStoreUpsertWithRetry](../../core/key-value-store/ask-key-value-store-upsert-with-retry.md) with `{ ifNotExists: true }`, plus [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) to find the events store name. Not a single action.
13
13
  - **Requires the store context** — provide it via [askEventDocProvideStore](./ask-event-doc-provide-store.md) / [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals).
@@ -31,7 +31,7 @@ function* askEventDocEventWrite(modelId: string, event: EventDocEvent): AskRespo
31
31
  | Parameter | Type | Description |
32
32
  | --- | --- | --- |
33
33
  | `modelId` | `string` | The document id — becomes the partition key (`pk`) of the stored event. |
34
- | `event` | `EventDocEvent` | A fully-formed event, including `payload.metadata.index` — the index becomes the sort key (`sk`) and the slot that is claimed conditionally. |
34
+ | `event` | `EventDocEvent` | A fully-formed event, including `payload.metadata.eventId` — the sortable id becomes the sort key (`sk`) and the slot that is claimed conditionally. |
35
35
 
36
36
  ## Returns
37
37
 
@@ -39,12 +39,12 @@ function* askEventDocEventWrite(modelId: string, event: EventDocEvent): AskRespo
39
39
 
40
40
  ## Notes
41
41
 
42
- - The stored shape is `{ pk: modelId, sk: index, data: event }`; the `EventDocStoredEvent` mapping is the only place that knows the key layout, keeping the domain event free of storage concerns.
43
- - Because the write is conditional, a losing concurrent writer surfaces `KeyValueStoreUpsertErrorTypeEnum.Conflict`. [askEventDocEventAppend](./ask-event-doc-event-append.md) is the layer that catches and re-laps on exactly that error.
42
+ - The stored shape is `{ pk: modelId, sk: eventId, data: event }`; the `EventDocStoredEvent` mapping is the only place that knows the key layout, keeping the domain event free of storage concerns.
43
+ - Because the write is conditional, an id collision surfaces `KeyValueStoreUpsertErrorTypeEnum.Conflict`. [askEventDocEventAppend](./ask-event-doc-event-append.md) does not catch or retry on it — with sortable ids minted uniquely per append, this is not an expected contention path.
44
44
 
45
45
  ## Related
46
46
 
47
- - [askEventDocEventAppend](./ask-event-doc-event-append.md) — the high-level append that computes the index, validates, and retries around this write.
47
+ - [askEventDocEventAppend](./ask-event-doc-event-append.md) — the high-level append that mints the sortable id and writes through this.
48
48
  - [askEventDocEventList / EventListAll / EventLast](./ask-event-doc-event-list.md) — reading events back.
49
49
  - [askKeyValueStoreUpsertWithRetry](../../core/key-value-store/ask-key-value-store-upsert-with-retry.md) — the underlying conditional upsert.
50
50
  - [askEventDocProvideStore](./ask-event-doc-provide-store.md) — provides the required store context.
@@ -25,8 +25,8 @@ export function* loadArticle(id: string) {
25
25
 
26
26
  An event document is never stored as a mutable blob. Its authoritative state is an **append-only log of events**; the document you read is *derived by folding that log*.
27
27
 
28
- - **Summary record** ([`EventDocSummary`](#the-summary-record)) — the queryable projection folded from the log's identity/lifecycle events (`INIT_STATE`, `SET_CODE`, `SET_NAME`, `PUBLISH`, …). It holds identity (`id`, `code`, `name`), audit fields, and a `versions` array. The document's editable **content** is folded separately (on the client) from the same log; the backend never reduces content.
29
- - **Draft vs published** — the tail (highest) version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes it and starts the next draft. Each version pointer records the `eventIndex` of its last event (its head), so folding events with index ≤ that head reconstructs the version's content as it was. `publishedAt` is when a version was published; `effectiveFrom` is when that publish takes effect (used for as-of time-travel).
28
+ - **Summary record** ([`EventDocSummary`](#the-summary-record)) — the queryable projection folded from the log's identity/lifecycle events (`INIT_STATE`, `SET_CODE`, `SET_NAME`, `PUBLISH`, `DELETE`, `RESTORE`, …). It holds identity (`id`, `code`, `name`), audit fields, and a `versions` array. Every field on it is derived from the log — including `deletedAt`, set and cleared by `DELETE`/`RESTORE` events rather than written directly — so the whole record can be dropped and rebuilt from the log at any time. The document's editable **content** is folded separately (on the client) from the same log; the backend never reduces content.
29
+ - **Draft vs published** — the tail (highest) version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes it and starts the next draft. Each version pointer records the `eventId` (a sortable id) of its last event (its head), so folding events whose `eventId` sorts at or before that head reconstructs the version's content as it was. `publishedAt` is when a version was published; `effectiveFrom` is when that publish takes effect (used for as-of time-travel).
30
30
  - **Code** — the caller-chosen, stable business key set at create (via `INIT_STATE`) and editable with `SET_CODE`. It stays constant across versions and is expected unique within the collection (and any owner scope), so you can address a document by `code` instead of its generated `id`.
31
31
 
32
32
  The version-pointer reads ([askEventDocGetDraft, askEventDocGetLatestPublished, askEventDocGetPublishedAsOf, askEventDocPublishedEventsAsOf](./ask-event-doc-get-draft.md)) resolve entries in this model.
@@ -41,7 +41,7 @@ type EventDocSummary = {
41
41
  name: string;
42
42
  createdAt: string; // ISO datetime
43
43
  updatedAt: string; // ISO datetime
44
- deletedAt?: string; // set by soft delete
44
+ deletedAt?: string; // derived from a DELETE event; cleared by RESTORE
45
45
  createdBy: string;
46
46
  updatedBy: string;
47
47
  versions: EventDocVersion[];
@@ -49,7 +49,7 @@ type EventDocSummary = {
49
49
 
50
50
  type EventDocVersion = {
51
51
  version: number;
52
- eventIndex: number; // log index of this version's head event
52
+ eventId: string; // sortable id of this version's head event
53
53
  publishedAt?: string; // unset while it is the tail draft
54
54
  effectiveFrom?: string; // when the publish takes effect (as-of selection)
55
55
  };
@@ -116,6 +116,6 @@ if (outcome.success) {
116
116
  - [askEventDocList](./ask-event-doc-list.md) — read every document in the collection.
117
117
  - [askEventDocGetByCode](./ask-event-doc-get-by-code.md) — look a document up by its business `code` instead of `id`.
118
118
  - [askEventDocGetDraft / …LatestPublished / …PublishedAsOf / …PublishedEventsAsOf](./ask-event-doc-get-draft.md) — resolve a document's versions.
119
- - [askEventDocCreate](./ask-event-doc-create.md) — create a document. [askEventDocSoftDelete](./ask-event-doc-soft-delete.md) — retire one.
119
+ - [askEventDocCreate](./ask-event-doc-create.md) — create a document. [askEventDocSoftDelete / askEventDocRestore](./ask-event-doc-soft-delete.md) — retire a document, or bring it back.
120
120
  - [defineEventDocSummary](../../../config/features/event-doc-summary.md) — declares the store these read from.
121
121
  - [askCatch](../../core/system/ask-catch.md) — handle thrown errors as a result object.
@@ -9,7 +9,7 @@ Resolves the current **draft** version pointer for a document by `id`, or `null`
9
9
 
10
10
  - **Built from:** [askEventDocGetById](./ask-event-doc-get-by-id.md) plus an in-memory version selector. Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
11
11
 
12
- First, a quick recap of the model (full detail on the [read-by-id page](./ask-event-doc-get-by-id.md#the-event-document-model)): an event document is derived by folding its event log. Each **version** pointer in the summary's `versions` array records the `eventIndex` of its last event (its head) and, once published, its `publishedAt` and `effectiveFrom` times. The tail version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes a version and opens the next draft.
12
+ First, a quick recap of the model (full detail on the [read-by-id page](./ask-event-doc-get-by-id.md#the-event-document-model)): an event document is derived by folding its event log. Each **version** pointer in the summary's `versions` array records the `eventId` of its last event (its head) and, once published, its `publishedAt` and `effectiveFrom` times. The tail version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes a version and opens the next draft.
13
13
 
14
14
  ```typescript
15
15
  import { askEventDocGetDraft } from 'quidproquo-features';
@@ -41,13 +41,13 @@ function* askEventDocGetDraft(id: string): AskResponse<Nullable<EventDocVersion>
41
41
  ```typescript
42
42
  type EventDocVersion = {
43
43
  version: number;
44
- eventIndex: number; // log index of this version's head event
44
+ eventId: string; // sortable id of this version's head event
45
45
  publishedAt?: string; // unset while it is the tail draft
46
46
  effectiveFrom?: string; // when the publish takes effect (as-of selection)
47
47
  };
48
48
  ```
49
49
 
50
- To fold or render a version's content, fold the log's events with index ≤ its `eventIndex`.
50
+ To fold or render a version's content, fold the log's events whose `eventId` sorts at or before the version's `eventId`.
51
51
 
52
52
  ---
53
53
 
@@ -85,7 +85,7 @@ function* askEventDocGetPublishedAsOf(
85
85
 
86
86
  ## askEventDocPublishedEventsAsOf
87
87
 
88
- Returns the **events** that make up the version published and *effective* at `clock` — the log truncated at that version's head. It resolves the version from the summary via `effectiveFrom` (when the publish takes effect, so a publish scheduled for the future stays invisible until then), then returns every event with `index <= version.eventIndex`. Fold the returned events to get the published, as-of-`clock` content — the generic backbone of a "render published" flow. (This mirrors `askEventDocGetPublishedAsOf`, which returns the version pointer rather than its events, and keys on `publishedAt` rather than `effectiveFrom`.)
88
+ Returns the **events** that make up the version published and *effective* at `clock` — the log truncated at that version's head. It resolves the version from the summary via `effectiveFrom` (when the publish takes effect, so a publish scheduled for the future stays invisible until then), then returns every event whose `eventId` sorts at or before `version.eventId`. Fold the returned events to get the published, as-of-`clock` content — the generic backbone of a "render published" flow. (This mirrors `askEventDocGetPublishedAsOf`, which returns the version pointer rather than its events, and keys on `publishedAt` rather than `effectiveFrom`.)
89
89
 
90
90
  ```typescript
91
91
  function* askEventDocPublishedEventsAsOf(