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.
- package/lib/commonjs/cli/runCreateQpqApp.js +14 -12
- package/lib/commonjs/cli/runCreateQpqApp.js.map +1 -1
- package/lib/commonjs/lib/getArgValue.d.ts +1 -0
- package/lib/commonjs/lib/getArgValue.js +14 -0
- package/lib/commonjs/lib/getArgValue.js.map +1 -0
- package/lib/commonjs/lib/{packageRoot.d.ts → getOwnPackageRoot.d.ts} +0 -1
- package/lib/commonjs/lib/{packageRoot.js → getOwnPackageRoot.js} +2 -7
- package/lib/commonjs/lib/getOwnPackageRoot.js.map +1 -0
- package/lib/commonjs/lib/getOwnVersion.d.ts +1 -0
- package/lib/commonjs/lib/getOwnVersion.js +15 -0
- package/lib/commonjs/lib/getOwnVersion.js.map +1 -0
- package/lib/commonjs/lib/{args.d.ts → getPositionalArgs.d.ts} +0 -1
- package/lib/commonjs/lib/getPositionalArgs.js +12 -0
- package/lib/commonjs/lib/getPositionalArgs.js.map +1 -0
- package/lib/commonjs/lib/index.d.ts +11 -0
- package/lib/commonjs/lib/index.js +28 -0
- package/lib/commonjs/lib/index.js.map +1 -0
- package/lib/commonjs/lib/listFilesRecursive.d.ts +1 -0
- package/lib/commonjs/lib/listFilesRecursive.js +27 -0
- package/lib/commonjs/lib/listFilesRecursive.js.map +1 -0
- package/lib/commonjs/lib/{prompts.js → promptSelect.js} +1 -1
- package/lib/commonjs/lib/promptSelect.js.map +1 -0
- package/lib/commonjs/lib/readJsonFile.d.ts +1 -0
- package/lib/commonjs/lib/readJsonFile.js +13 -0
- package/lib/commonjs/lib/readJsonFile.js.map +1 -0
- package/lib/commonjs/lib/replaceInFileExact.d.ts +1 -0
- package/lib/commonjs/lib/replaceInFileExact.js +18 -0
- package/lib/commonjs/lib/replaceInFileExact.js.map +1 -0
- package/lib/commonjs/lib/replaceInFiles.d.ts +1 -0
- package/lib/commonjs/lib/replaceInFiles.js +32 -0
- package/lib/commonjs/lib/replaceInFiles.js.map +1 -0
- package/lib/commonjs/lib/writeJsonFile.d.ts +1 -0
- package/lib/commonjs/lib/writeJsonFile.js +12 -0
- package/lib/commonjs/lib/writeJsonFile.js.map +1 -0
- package/lib/commonjs/steps/001_preflight.js +3 -3
- package/lib/commonjs/steps/001_preflight.js.map +1 -1
- package/lib/commonjs/steps/002_copyTemplate.js +1 -1
- package/lib/commonjs/steps/002_copyTemplate.js.map +1 -1
- package/lib/commonjs/steps/003_deleteDocusaurus.js +5 -4
- package/lib/commonjs/steps/003_deleteDocusaurus.js.map +1 -1
- package/lib/commonjs/steps/005_applyAppIdentity.js +12 -9
- package/lib/commonjs/steps/005_applyAppIdentity.js.map +1 -1
- package/lib/commonjs/steps/006_applyDomain.js +6 -4
- package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
- package/lib/commonjs/steps/007_pinRegistryVersions.js +5 -4
- package/lib/commonjs/steps/007_pinRegistryVersions.js.map +1 -1
- package/lib/commonjs/steps/008_transpileToJavaScript.js +19 -17
- package/lib/commonjs/steps/008_transpileToJavaScript.js.map +1 -1
- package/lib/commonjs/steps/009_restoreGitignore.js +2 -2
- package/lib/commonjs/steps/009_restoreGitignore.js.map +1 -1
- package/lib/commonjs/steps/010_gitInit.js +1 -1
- package/lib/commonjs/steps/010_gitInit.js.map +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
- package/lib/commonjs/steps/index.js +1 -1
- package/lib/commonjs/steps/index.js.map +1 -1
- package/lib/commonjs/types/AppLanguage.d.ts +4 -0
- package/lib/commonjs/{types.js → types/AppLanguage.js} +1 -1
- package/lib/commonjs/types/AppLanguage.js.map +1 -0
- package/lib/commonjs/types/CreateQpqAppAnswers.d.ts +8 -0
- package/lib/commonjs/types/CreateQpqAppAnswers.js +3 -0
- package/lib/commonjs/types/CreateQpqAppAnswers.js.map +1 -0
- package/lib/commonjs/types/CreateQpqAppStep.d.ts +7 -0
- package/lib/commonjs/types/CreateQpqAppStep.js +3 -0
- package/lib/commonjs/types/CreateQpqAppStep.js.map +1 -0
- package/lib/commonjs/types/StepContext.d.ts +7 -0
- package/lib/commonjs/types/StepContext.js +3 -0
- package/lib/commonjs/types/StepContext.js.map +1 -0
- package/lib/commonjs/types/index.d.ts +4 -0
- package/lib/commonjs/types/index.js +21 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/esm/cli/runCreateQpqApp.js +8 -6
- package/lib/esm/cli/runCreateQpqApp.js.map +1 -1
- package/lib/esm/lib/getArgValue.d.ts +1 -0
- package/lib/esm/lib/getArgValue.js +10 -0
- package/lib/esm/lib/getArgValue.js.map +1 -0
- package/lib/esm/lib/{packageRoot.d.ts → getOwnPackageRoot.d.ts} +0 -1
- package/lib/esm/lib/{packageRoot.js → getOwnPackageRoot.js} +1 -5
- package/lib/esm/lib/getOwnPackageRoot.js.map +1 -0
- package/lib/esm/lib/getOwnVersion.d.ts +1 -0
- package/lib/esm/lib/getOwnVersion.js +8 -0
- package/lib/esm/lib/getOwnVersion.js.map +1 -0
- package/lib/esm/lib/{args.d.ts → getPositionalArgs.d.ts} +0 -1
- package/lib/esm/lib/getPositionalArgs.js +8 -0
- package/lib/esm/lib/getPositionalArgs.js.map +1 -0
- package/lib/esm/lib/index.d.ts +11 -0
- package/lib/esm/lib/index.js +12 -0
- package/lib/esm/lib/index.js.map +1 -0
- package/lib/esm/lib/listFilesRecursive.d.ts +1 -0
- package/lib/esm/lib/listFilesRecursive.js +20 -0
- package/lib/esm/lib/listFilesRecursive.js.map +1 -0
- package/lib/esm/lib/{prompts.js → promptSelect.js} +1 -1
- package/lib/esm/lib/promptSelect.js.map +1 -0
- package/lib/esm/lib/readJsonFile.d.ts +1 -0
- package/lib/esm/lib/readJsonFile.js +6 -0
- package/lib/esm/lib/readJsonFile.js.map +1 -0
- package/lib/esm/lib/replaceInFileExact.d.ts +1 -0
- package/lib/esm/lib/replaceInFileExact.js +11 -0
- package/lib/esm/lib/replaceInFileExact.js.map +1 -0
- package/lib/esm/lib/replaceInFiles.d.ts +1 -0
- package/lib/esm/lib/replaceInFiles.js +25 -0
- package/lib/esm/lib/replaceInFiles.js.map +1 -0
- package/lib/esm/lib/writeJsonFile.d.ts +1 -0
- package/lib/esm/lib/writeJsonFile.js +5 -0
- package/lib/esm/lib/writeJsonFile.js.map +1 -0
- package/lib/esm/steps/001_preflight.js +3 -3
- package/lib/esm/steps/001_preflight.js.map +1 -1
- package/lib/esm/steps/002_copyTemplate.js +1 -1
- package/lib/esm/steps/002_copyTemplate.js.map +1 -1
- package/lib/esm/steps/003_deleteDocusaurus.js +3 -2
- package/lib/esm/steps/003_deleteDocusaurus.js.map +1 -1
- package/lib/esm/steps/005_applyAppIdentity.js +6 -3
- package/lib/esm/steps/005_applyAppIdentity.js.map +1 -1
- package/lib/esm/steps/006_applyDomain.js +3 -1
- package/lib/esm/steps/006_applyDomain.js.map +1 -1
- package/lib/esm/steps/007_pinRegistryVersions.js +3 -2
- package/lib/esm/steps/007_pinRegistryVersions.js.map +1 -1
- package/lib/esm/steps/008_transpileToJavaScript.js +10 -8
- package/lib/esm/steps/008_transpileToJavaScript.js.map +1 -1
- package/lib/esm/steps/009_restoreGitignore.js +1 -1
- package/lib/esm/steps/009_restoreGitignore.js.map +1 -1
- package/lib/esm/steps/010_gitInit.js +1 -1
- package/lib/esm/steps/010_gitInit.js.map +1 -1
- package/lib/esm/steps/013_printNextSteps.js +1 -1
- package/lib/esm/steps/013_printNextSteps.js.map +1 -1
- package/lib/esm/steps/index.js +1 -1
- package/lib/esm/steps/index.js.map +1 -1
- package/lib/esm/types/AppLanguage.d.ts +4 -0
- package/lib/esm/{types.js → types/AppLanguage.js} +1 -1
- package/lib/esm/types/AppLanguage.js.map +1 -0
- package/lib/esm/types/CreateQpqAppAnswers.d.ts +8 -0
- package/lib/esm/types/CreateQpqAppAnswers.js +2 -0
- package/lib/esm/types/CreateQpqAppAnswers.js.map +1 -0
- package/lib/esm/types/CreateQpqAppStep.d.ts +7 -0
- package/lib/esm/types/CreateQpqAppStep.js +2 -0
- package/lib/esm/types/CreateQpqAppStep.js.map +1 -0
- package/lib/esm/types/StepContext.d.ts +7 -0
- package/lib/esm/types/StepContext.js +2 -0
- package/lib/esm/types/StepContext.js.map +1 -0
- package/lib/esm/types/index.d.ts +4 -0
- package/lib/esm/types/index.js +5 -0
- package/lib/esm/types/index.js.map +1 -0
- package/package.json +5 -4
- package/template/docusaurus/docs/actions/core/crypto/_category_.json +7 -0
- package/template/docusaurus/docs/actions/core/crypto/ask-crypto-decrypt.md +61 -0
- package/template/docusaurus/docs/actions/core/crypto/ask-crypto-encrypt.md +66 -0
- package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-scan-all-scopes.md +98 -0
- package/template/docusaurus/docs/actions/features/_category_.json +1 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +2 -2
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +2 -2
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +17 -15
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +9 -9
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +7 -7
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +5 -5
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +4 -4
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-provide-store.md +6 -0
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-references.md +47 -0
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-soft-delete.md +48 -10
- package/template/docusaurus/docs/actions/features/event-doc-transfer/_category_.json +1 -0
- package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-bundle-apply.md +67 -0
- package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-bundle-plan.md +67 -0
- package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-manifest.md +64 -0
- package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-transfer-export.md +67 -0
- package/template/docusaurus/docs/actions/xstate/ask-state-machine-send-event.md +6 -6
- package/template/docusaurus/docs/config/core/crypto-key.md +53 -0
- package/template/docusaurus/docs/config/core/key-value-store.md +53 -1
- package/template/docusaurus/docs/config/features/event-doc-routes.md +5 -1
- package/template/docusaurus/docs/config/features/event-doc-summary.md +7 -6
- package/template/docusaurus/docs/config/features/event-doc-transfer.md +81 -0
- package/template/docusaurus/docs/config/features/event-doc.md +1 -0
- package/template/docusaurus/docs/config/features/tenanted-event-doc-transfer.md +59 -0
- package/template/docusaurus/docs/config/features/tenanted-event-doc.md +1 -0
- package/template/docusaurus/docs/config/webserver/migration.md +4 -0
- package/template/package.json +1 -0
- package/lib/commonjs/lib/args.js +0 -21
- package/lib/commonjs/lib/args.js.map +0 -1
- package/lib/commonjs/lib/files.d.ts +0 -5
- package/lib/commonjs/lib/files.js +0 -65
- package/lib/commonjs/lib/files.js.map +0 -1
- package/lib/commonjs/lib/packageRoot.js.map +0 -1
- package/lib/commonjs/lib/prompts.js.map +0 -1
- package/lib/commonjs/types.d.ts +0 -22
- package/lib/commonjs/types.js.map +0 -1
- package/lib/esm/lib/args.js +0 -16
- package/lib/esm/lib/args.js.map +0 -1
- package/lib/esm/lib/files.d.ts +0 -5
- package/lib/esm/lib/files.js +0 -54
- package/lib/esm/lib/files.js.map +0 -1
- package/lib/esm/lib/packageRoot.js.map +0 -1
- package/lib/esm/lib/prompts.js.map +0 -1
- package/lib/esm/types.d.ts +0 -22
- package/lib/esm/types.js.map +0 -1
- /package/lib/commonjs/lib/{prompts.d.ts → promptSelect.d.ts} +0 -0
- /package/lib/esm/lib/{prompts.d.ts → promptSelect.d.ts} +0 -0
|
@@ -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 @@
|
|
|
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.
|
|
4
|
-
"description": "Scaffold a new quidproquo 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": "
|
|
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.
|
|
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.
|
package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-scan-all-scopes.md
ADDED
|
@@ -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." } }
|
package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md
CHANGED
|
@@ -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 (`
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (`
|
|
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.
|
|
62
|
-
| `payload.metadata.clientMessageId` | `string` | A client-generated id used for dedup:
|
|
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
|
|
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 `
|
|
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
|
-
- **
|
|
86
|
-
- **
|
|
87
|
-
- **
|
|
88
|
-
-
|
|
89
|
-
-
|
|
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
|
|
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 `
|
|
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,
|
|
21
|
-
const page = yield* askEventDocEventList(docId, {
|
|
22
|
-
return page.items; // events
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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:
|
|
43
|
-
- Because the write is conditional,
|
|
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
|
|
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 `
|
|
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; //
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|