create-qpq-app 0.1.13 → 0.1.15

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 (25) hide show
  1. package/package.json +2 -2
  2. package/template/docusaurus/docs/actions/core/dynamic-functions/_category_.json +1 -0
  3. package/template/docusaurus/docs/actions/core/dynamic-functions/ask-dynamic-function-execute.md +89 -0
  4. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-query.md +1 -0
  5. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +3 -3
  6. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +1 -1
  7. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +9 -1
  8. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-generate-asset-upload-url.md +42 -3
  9. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +1 -1
  10. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +5 -5
  11. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-list.md +57 -11
  12. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-provide-store.md +6 -8
  13. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-references-from-state.md +50 -0
  14. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-references.md +11 -7
  15. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-render-for-collection.md +54 -0
  16. package/template/docusaurus/docs/actions/features/event-doc-transfer/ask-event-doc-manifest.md +1 -1
  17. package/template/docusaurus/docs/config/core/dynamic-functions.md +65 -0
  18. package/template/docusaurus/docs/config/core/queue.md +1 -1
  19. package/template/docusaurus/docs/config/features/admin-session-event-doc.md +1 -1
  20. package/template/docusaurus/docs/config/features/event-doc-routes.md +17 -16
  21. package/template/docusaurus/docs/config/features/event-doc-summary.md +26 -6
  22. package/template/docusaurus/docs/config/features/event-doc-transfer.md +16 -19
  23. package/template/docusaurus/docs/config/features/event-doc.md +39 -24
  24. package/template/docusaurus/docs/config/features/tenanted-event-doc-transfer.md +3 -4
  25. package/template/docusaurus/docs/config/features/tenanted-event-doc.md +9 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
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",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.13.13",
55
- "quidproquo-tsconfig": "0.1.13"
55
+ "quidproquo-tsconfig": "0.1.15"
56
56
  },
57
57
  "bin": {
58
58
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -0,0 +1 @@
1
+ { "label": "Dynamic Functions", "link": { "type": "generated-index", "description": "Invoke a member of a registered functions object by name from within a story." } }
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: askDynamicFunctionExecute
3
+ description: Invoke a member of a registered dynamic-functions object by name and return its result, running generator members in-process as a nested story.
4
+ ---
5
+
6
+ # askDynamicFunctionExecute
7
+
8
+ Invokes one member of a [dynamic functions](../../../config/core/dynamic-functions.md) object by name, passing it positional args, and returns whatever the member resolves to. A **plain or async member** is called and (if needed) awaited directly. A **generator member** — the shape of every qpq story — is instead driven through the runtime as a nested story (one depth deeper), sharing the caller's action processors and session context, and its story result is returned.
9
+
10
+ - **Action type:** `DynamicFunctionsActionType.Execute`
11
+
12
+ ```typescript
13
+ import { askDynamicFunctionExecute } from 'quidproquo-core';
14
+
15
+ import { templateEventDoc } from '../eventDocs/templateEventDoc';
16
+
17
+ export function* foldTemplate(events: unknown[]) {
18
+ const views = yield* askDynamicFunctionExecute<typeof templateEventDoc>(
19
+ 'templateEventDoc',
20
+ 'foldSnapshotViews',
21
+ events,
22
+ );
23
+
24
+ return views;
25
+ }
26
+ ```
27
+
28
+ ## Signature
29
+
30
+ ```typescript
31
+ function* askDynamicFunctionExecute<TFunctions extends DynamicFunctions, TName extends keyof TFunctions & string>(
32
+ dynamicFunctionsName: string,
33
+ functionName: TName,
34
+ ...args: Parameters<TFunctions[TName]>
35
+ ): AskResponse<DynamicFunctionResult<TFunctions[TName]>>;
36
+ ```
37
+
38
+ The `TFunctions` generic is the type of the registered functions object — pass it explicitly (e.g. `askDynamicFunctionExecute<typeof templateEventDoc>(...)`) so `functionName` and `args` are checked against its members and the return type is inferred.
39
+
40
+ ## Parameters
41
+
42
+ | Parameter | Type | Description |
43
+ | --- | --- | --- |
44
+ | `dynamicFunctionsName` | `string` | Name of the dynamic functions object to invoke — must match the `dynamicFunctionsName` of a surface registered with [defineDynamicFunctions](../../../config/core/dynamic-functions.md). |
45
+ | `functionName` | `TName` | Name of the member to call on the registered object. |
46
+ | `args` | `Parameters<TFunctions[TName]>` | Positional arguments passed to the member, typed from `TFunctions`. |
47
+
48
+ ## Returns
49
+
50
+ `DynamicFunctionResult<TFunctions[TName]>` — the value the member resolves to:
51
+
52
+ - A **generator** member (a story) resolves to its story return value.
53
+ - A **promise-returning** member resolves to its awaited value.
54
+ - Any other member resolves to its plain return value.
55
+
56
+ ## Errors
57
+
58
+ `askDynamicFunctionExecute` fails with `DynamicFunctionsExecuteErrorTypeEnum`:
59
+
60
+ | Error | Cause |
61
+ | --- | --- |
62
+ | `DynamicFunctionsNotFound` | No `defineDynamicFunctions` setting is registered under `dynamicFunctionsName`. |
63
+ | `ModuleLoadFailed` | The registered module could not be loaded at runtime. |
64
+ | `FunctionNotFound` | `functionName` is not an own enumerable function on the loaded object (inherited/prototype members don't count). |
65
+ | `FunctionThrew` | A plain or async member threw or rejected. |
66
+
67
+ If the invoked member is a generator and its own story throws, that error propagates to the caller **with its original error type** (not wrapped in one of the above), with the function name added to the error stack. Catch failures with `askCatch`:
68
+
69
+ ```typescript
70
+ const outcome = yield* askCatch(
71
+ askDynamicFunctionExecute<typeof templateEventDoc>('templateEventDoc', 'foldSnapshotViews', events),
72
+ );
73
+
74
+ if (outcome.success) {
75
+ const views = outcome.result;
76
+ } else {
77
+ // outcome.error.errorType / outcome.error.errorText
78
+ }
79
+ ```
80
+
81
+ ## Notes
82
+
83
+ - The invoked member runs **in the same process** as the caller — a generator member runs nested one level deeper, not as a separate deployed service — dynamic functions add no infrastructure of their own.
84
+ - `createDynamicFunctionCaller` (also exported from `quidproquo-core`) wraps this action in a typed proxy so call sites read like a normal method call instead of a string-keyed action: `const caller = createDynamicFunctionCaller<typeof templateEventDoc>('templateEventDoc'); yield* caller.foldSnapshotViews(events);` yields the exact same action as calling `askDynamicFunctionExecute` directly.
85
+
86
+ ## Related
87
+
88
+ - [defineDynamicFunctions](../../../config/core/dynamic-functions.md) — registers the functions object this action invokes members on.
89
+ - [askInlineFunctionExecute](../../core/inline-function/ask-inline-function-execute.md) — the single-function predecessor to this action.
@@ -55,6 +55,7 @@ function* askKeyValueStoreQuery<KvsItem>(
55
55
  | `sortAscending` | `boolean` | `true` | Order results by the sort key. `false` returns the newest/highest first. |
56
56
  | `limit` | `number` | – | Maximum number of records to return in this page. |
57
57
  | `nextPageKey` | `string` | – | Opaque cursor from a previous page's `nextPageKey`; pass it to fetch the following page. |
58
+ | `consistentRead` | `boolean` | `false` | Strongly consistent (read-your-own-writes) read. Costs roughly double the read capacity on DynamoDB and cannot be served from a global secondary index. Use it when a caller just wrote and is now querying to decide something (e.g. folding a log right after appending to it); leave it off for ordinary reads. |
58
59
  | `ttlInSeconds` | `number` | – | Accepted but not implemented: no processor currently applies a TTL to query results, so setting it has no effect. |
59
60
  | `scope` | `string` | – | Optional storage scope. The processor composes it into the partition-key conditions, so the query only matches records written under the same scope (used by tenant/scoped features). Requires a string-typed partition key, and the key condition must constrain the partition key. |
60
61
 
@@ -57,7 +57,7 @@ A point-in-time snapshot of who produced an event, captured server-side at appen
57
57
 
58
58
  ## Notes
59
59
 
60
- - The record is validated against `eventDocSummarySchema` before it is written; a schema violation throws (`ErrorTypeEnum` from quidproquo-core).
60
+ - The record is validated against `eventDocSummaryViewSchema` before it is written; a schema violation throws (`ErrorTypeEnum` from quidproquo-core).
61
61
  - Not concurrency-safe against duplicate codes on its own. If two callers may create the same `code` at once, prefer [askEventDocGetByCodeOrCreate](./ask-event-doc-get-by-code.md#askeventdocgetbycodeorcreate) and serialise, or add a conditional create.
62
62
 
63
63
  ---
@@ -91,12 +91,12 @@ Use `askEventDocCreate` unless you are building a custom create flow that needs
91
91
  The bare storage write for a summary record — a thin wrapper over the key-value store upsert (with retry). Business rules and validation live in the logic layer (e.g. `askEventDocCreate`, [askEventDocSoftDelete](./ask-event-doc-soft-delete.md)); this just persists whatever record you hand it.
92
92
 
93
93
  ```typescript
94
- function* askEventDocUpsert(model: EventDocSummary): AskResponse<void>;
94
+ function* askEventDocUpsert(view: EventDocSummaryView): AskResponse<void>;
95
95
  ```
96
96
 
97
97
  | Parameter | Type | Description |
98
98
  | --- | --- | --- |
99
- | `model` | `EventDocSummary` | The full summary record to write (create or overwrite). |
99
+ | `view` | `EventDocSummaryView` | The summary view to write (create or overwrite) — `type` is resolved from the store context and stamped on internally. |
100
100
 
101
101
  **Returns** `void`.
102
102
 
@@ -84,7 +84,7 @@ What the client POSTs to append an event. `modelId` and the server-stamped prove
84
84
 
85
85
  ## Notes
86
86
 
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.
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 `validators` registry (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
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
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
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.
@@ -46,6 +46,9 @@ function* askEventDocEventList(
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
+ | `upToEventId` | `string` | — | Return only events whose event id sorts at or before this one (inclusive) — the log prefix up to a known event, for folding a document as of that event (a snapshot). Not combinable with `afterEventId`: a key condition holds one sort-key range. |
50
+ | `sortDescending` | `boolean` | `false` | Newest first, for a display read that walks backwards in time (e.g. a history panel's latest-page-then-load-older). Folding reads never set this — a fold consumes the log in order. |
51
+ | `consistentRead` | `boolean` | `false` | Strongly consistent read. Needed by a caller that just appended and is now folding to decide something — the default eventually-consistent read can otherwise miss that caller's own most recent event. Costs roughly double the read capacity, so leave it off for ordinary reads. |
49
52
 
50
53
  ### Returns
51
54
 
@@ -66,7 +69,10 @@ export function* fullHistory(docId: string) {
66
69
  ### Signature
67
70
 
68
71
  ```typescript
69
- function* askEventDocEventListAll(modelId: string): AskResponse<EventDocEvent[]>;
72
+ function* askEventDocEventListAll(
73
+ modelId: string,
74
+ options?: { consistentRead?: boolean; upToEventId?: string },
75
+ ): AskResponse<EventDocEvent[]>;
70
76
  ```
71
77
 
72
78
  ### Parameters
@@ -74,6 +80,8 @@ function* askEventDocEventListAll(modelId: string): AskResponse<EventDocEvent[]>
74
80
  | Parameter | Type | Description |
75
81
  | --- | --- | --- |
76
82
  | `modelId` | `string` | The document id to read the full log for. |
83
+ | `options.consistentRead` | `boolean` | Optional, default `false`. Strongly consistent read — for a caller that just appended and is now folding to decide something, so it does not miss its own most recent event. Leave off for ordinary reads; it doubles the read cost. |
84
+ | `options.upToEventId` | `string` | Optional. Return only the log prefix up to and including this event id — for folding a document as of a known event (a snapshot). |
77
85
 
78
86
  ### Returns
79
87
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: askEventDocGenerateAssetUploadUrl
3
- description: Manage a document's immutable assets — mint presigned upload/download URLs, or write server-generated bytes directly.
3
+ description: Manage a document's immutable assets and size-aware values — mint presigned upload/download URLs, write server-generated bytes directly, or record a value inline vs as an asset depending on its size.
4
4
  ---
5
5
 
6
6
  # Document assets
7
7
 
8
8
  A document can point at binary **assets** — images, fonts, rendered artifacts — without embedding the bytes. Each asset is stored immutably on the collection's storage drive under `<docId>/assets/<guid>`, and the document records only a reference (the guid) inside a domain event. Because every upload gets a fresh guid, a re-upload is a new blob and prior references stay addressable for history and rollback.
9
9
 
10
- These three helpers cover the two flows: presigned URLs for a browser to upload/download bytes directly, and a server-side write for when the backend already holds the bytes.
10
+ These helpers cover three flows: presigned URLs for a browser to upload/download bytes directly, a server-side write for when the backend already holds the bytes, and a size-aware write that decides between inline and asset storage for values of unpredictable size.
11
11
 
12
12
  - **Requires the store context** — provide it via [askEventDocProvideStore](./ask-event-doc-provide-store.md) / [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals). The drive name is resolved from that context.
13
13
  - **Built from:** the core file secure-URL / binary-write actions on the collection's storage drive, plus [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore). Not single actions.
@@ -115,10 +115,49 @@ function* askEventDocWriteAsset(
115
115
 
116
116
  `AskResponse<EventDocAssetRef>` — `{ guid, filename, mimetype }`, a first-class reference to record in a domain event so the document points at the bytes without embedding them.
117
117
 
118
+ ## askEventDocWriteValue
119
+
120
+ Records a **value** (data of unpredictable size — a node output, a variable, a trigger input) for an event: inline when it is small enough, or as an asset when it is not. This is the size-aware counterpart to `askEventDocWriteAsset` above, which stays the right choice for genuine files where a reference is always correct.
121
+
122
+ ```typescript
123
+ import { askEventDocWriteValue } from 'quidproquo-features';
124
+
125
+ export function* recordNodeOutput(docId: string, nodeId: string, output: unknown) {
126
+ const ref = yield* askEventDocWriteValue(docId, output, `${nodeId}.json`);
127
+ // ref is either { kind: 'inline', value } or { kind: 'asset', guid, filename, mimetype }
128
+ return ref;
129
+ }
130
+ ```
131
+
132
+ ### Signature
133
+
134
+ ```typescript
135
+ function* askEventDocWriteValue(
136
+ docId: string,
137
+ value: unknown,
138
+ filename: string,
139
+ maxInlineBytes?: number,
140
+ ): AskResponse<EventDocValueRef>;
141
+ ```
142
+
143
+ ### Parameters
144
+
145
+ | Parameter | Type | Description |
146
+ | --- | --- | --- |
147
+ | `docId` | `string` | The document the value belongs to — used for the asset blob key when the value doesn't fit inline. |
148
+ | `value` | `unknown` | The value to record. Serialised with `JSON.stringify`; `undefined` is normalised to `null`. |
149
+ | `filename` | `string` | Filename to use if the value is written as an asset. |
150
+ | `maxInlineBytes` | `number` | Optional, default 4KB. The per-value inline ceiling. A caller recording several values against one event's combined size budget can pass the *remaining* budget so a value that no longer fits falls back to an asset; passing `0` always forces an asset. |
151
+
152
+ ### Returns
153
+
154
+ `AskResponse<EventDocValueRef>` — either `{ kind: 'inline', value }` (the value travelled in the event, nothing to fetch) or `{ kind: 'asset', guid, filename, mimetype }` (fetch it like any other asset reference).
155
+
118
156
  ## Notes
119
157
 
120
- - All three resolve the drive from the store context, so a missing context throws (see [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore)).
158
+ - All four resolve the drive from the store context, so a missing context throws (see [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore)).
121
159
  - Assets are immutable: guid-named so a re-upload is always a fresh blob; the old one stays addressable for history/rollback. Assets live under `<docId>/assets/<guid>`; derived, disposable artifacts use a sibling `<docId>/runtime/<...>` prefix written elsewhere.
160
+ - On the read side, `isInlineEventDocValueRef`, `readInlineEventDocValueRefs`, and `resolveEventDocValueRef` (all in `quidproquo-features`) are the shared helpers for branching on a ref's `kind` and resolving it against fetched asset snapshots — use them rather than checking `ref.kind` by hand, so every reader agrees on what "available" means.
122
161
 
123
162
  ## Related
124
163
 
@@ -29,7 +29,7 @@ An event document is never stored as a mutable blob. Its authoritative state is
29
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
- The version-pointer reads ([askEventDocGetDraft, askEventDocGetLatestPublished, askEventDocGetPublishedAsOf, askEventDocPublishedEventsAsOf](./ask-event-doc-get-draft.md)) resolve entries in this model.
32
+ The version-pointer reads ([askEventDocGetDraft, askEventDocGetLatestPublished, askEventDocGetPublishedAsOf, askEventDocPublishedVersionAsOf](./ask-event-doc-get-draft.md)) resolve entries in this model.
33
33
 
34
34
  ### The summary record
35
35
 
@@ -83,15 +83,15 @@ function* askEventDocGetPublishedAsOf(
83
83
 
84
84
  **Returns** `EventDocVersion | null` — the highest version with `publishedAt <= clock`, or `null`.
85
85
 
86
- ## askEventDocPublishedEventsAsOf
86
+ ## askEventDocPublishedVersionAsOf
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 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`.)
88
+ Returns the version published and *effective* at `clock`, together with the document **state** at that version's head — folded snapshot-seeded (via `askEventDocDocumentStateAsOf`), so cost tracks the gap since the nearest snapshot rather than the whole log. 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 folds the state as of `version.eventId`. Use `state` directly to render the published, as-of-`clock` content, and `version.publishedAt` to pin the doc's own linked assets to the moment it was published — the generic backbone of a "render published" flow. (This mirrors `askEventDocGetPublishedAsOf`, which returns only the version pointer, and keys on `publishedAt` rather than `effectiveFrom`.)
89
89
 
90
90
  ```typescript
91
- function* askEventDocPublishedEventsAsOf(
91
+ function* askEventDocPublishedVersionAsOf(
92
92
  id: string,
93
93
  clock: QpqIsoDateTime,
94
- ): AskResponse<Nullable<EventDocEvent[]>>;
94
+ ): AskResponse<Nullable<EventDocVersionState>>;
95
95
  ```
96
96
 
97
97
  | Parameter | Type | Description |
@@ -99,7 +99,7 @@ function* askEventDocPublishedEventsAsOf(
99
99
  | `id` | `string` | Id of the document. |
100
100
  | `clock` | `QpqIsoDateTime` | ISO-8601 timestamp to resolve the effective version as-of. |
101
101
 
102
- **Returns** `EventDocEvent[] | null` — the truncated event log for the effective version, or `null` when the document is missing/deleted or nothing is effective yet. Reads the full log via `askEventDocEventListAll` (quidproquo-features).
102
+ **Returns** `EventDocVersionState | null` — `{ version, state }` for the effective version, or `null` when the document is missing/deleted, nothing is effective yet, or the version's events are gone (a rewritten log).
103
103
 
104
104
  ## Related
105
105
 
@@ -1,13 +1,17 @@
1
1
  ---
2
2
  title: askEventDocList
3
- description: List every event document in the current collection, newest-updated first, hiding soft-deleted rows by default.
3
+ description: List event documents in the current collection, newest-updated first, hiding soft-deleted rows by default — as a full array or one page at a time.
4
4
  ---
5
5
 
6
- # askEventDocList
6
+ # Listing a collection
7
7
 
8
- Lists the summary records of every document in the current collection (all rows sharing the collection's `type`), sorted by `updatedAt` **descending** (most recently updated first). Soft-deleted documents are hidden unless you opt in.
8
+ Two ways to list the summary records of a collection (all rows sharing the collection's `type`), sorted by `updatedAt` **descending** (most recently updated first). Soft-deleted documents are hidden unless you opt in. Both require the store context — call inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
9
9
 
10
- - **Built from:** a key-value store query over the collection's `type`, filtered and sorted in memory. Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
10
+ ## askEventDocList
11
+
12
+ Reads the **whole** collection into memory, filters and sorts it, and returns it as a flat array. Fine for a handful of documents; costs read capacity proportional to the whole collection, so it does not scale to a large collection — use [askEventDocListPage](#askeventdoclistpage) for that.
13
+
14
+ - **Built from:** a key-value store query over the collection's `type`, filtered and sorted in memory.
11
15
 
12
16
  ```typescript
13
17
  import { askEventDocList } from 'quidproquo-features';
@@ -18,7 +22,7 @@ export function* listArticles() {
18
22
  }
19
23
  ```
20
24
 
21
- ## Signature
25
+ ### Signature
22
26
 
23
27
  ```typescript
24
28
  function* askEventDocList<T extends EventDocSummary = EventDocSummary>(
@@ -26,9 +30,9 @@ function* askEventDocList<T extends EventDocSummary = EventDocSummary>(
26
30
  ): AskResponse<T[]>;
27
31
  ```
28
32
 
29
- ## Parameters
33
+ ### Parameters
30
34
 
31
- ### `options` — `EventDocListOptions` (optional)
35
+ #### `options` — `EventDocListOptions` (optional)
32
36
 
33
37
  | Property | Type | Default | Description |
34
38
  | --- | --- | --- | --- |
@@ -36,18 +40,60 @@ function* askEventDocList<T extends EventDocSummary = EventDocSummary>(
36
40
 
37
41
  The generic `T` lets callers narrow to a collection-specific extension of [`EventDocSummary`](./ask-event-doc-get-by-id.md#the-summary-record); it defaults to `EventDocSummary`.
38
42
 
39
- ## Returns
43
+ ### Returns
40
44
 
41
45
  `EventDocSummary[]` — the collection's records, sorted by `updatedAt` descending. Empty array when the collection has no (visible) documents.
42
46
 
43
- ## Notes
47
+ ### Notes
44
48
 
45
49
  - Ordering is applied in memory, but the summary store carries a `(type, updatedAt)` index so this stays efficient.
46
50
  - Because there is no secondary index on `code`, the code-based reads ([askEventDocGetByCode](./ask-event-doc-get-by-code.md) and friends) list the collection via this action and filter in memory.
47
51
 
52
+ ## askEventDocListPage
53
+
54
+ Reads **one page** of the collection straight off the store, newest first, without reading or holding the rest of the collection in memory. This is what the built-in `GET {basePath}` list route uses.
55
+
56
+ - **Built from:** [askKeyValueStoreQuery](../../core/key-value-store/ask-key-value-store-query.md) against the summary store, ordered by its `(type, updatedAt)` index (descending), with soft-deleted rows excluded by a query filter.
57
+
58
+ ```typescript
59
+ import { askEventDocListPage } from 'quidproquo-features';
60
+
61
+ export function* listArticlesPage(nextPageKey?: string) {
62
+ return yield* askEventDocListPage({ nextPageKey }); // QpqPagedData<EventDocSummary>
63
+ }
64
+ ```
65
+
66
+ ### Signature
67
+
68
+ ```typescript
69
+ function* askEventDocListPage<T extends EventDocSummary = EventDocSummary>(
70
+ options?: EventDocListPageOptions,
71
+ ): AskResponse<QpqPagedData<T>>;
72
+ ```
73
+
74
+ ### Parameters
75
+
76
+ #### `options` — `EventDocListPageOptions` (optional)
77
+
78
+ | Property | Type | Default | Description |
79
+ | --- | --- | --- | --- |
80
+ | `includeDeleted` | `boolean` | `false` | When `true`, soft-deleted documents are included. When `false` (default), they are excluded by a query filter. |
81
+ | `limit` | `number` | `10` | Maximum number of records to return in this page. |
82
+ | `nextPageKey` | `string` | — | Opaque cursor from a previous page's `nextPageKey`; pass it to fetch the following page. |
83
+
84
+ ### Returns
85
+
86
+ `AskResponse<QpqPagedData<T>>` — `{ items: T[]; nextPageKey?: string }`, one page ordered by `updatedAt` descending.
87
+
88
+ ### Notes
89
+
90
+ - Because the exclusion of soft-deleted rows is a query filter (applied **after** rows are read), a page can come back shorter than `limit` while more pages remain. Page on the presence of `nextPageKey`, never on item count, or documents past a short page are silently hidden.
91
+ - There is no jump-to-page-N or total count without reading the whole collection — the store hands back an opaque "continue from here" cursor, not an offset, so pages are walked rather than addressed.
92
+
48
93
  ## Related
49
94
 
50
95
  - [askEventDocGetById / askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md) — read a single document.
51
96
  - [askEventDocGetByCode](./ask-event-doc-get-by-code.md) — find one document by its business `code`.
52
- - [askEventDocSoftDelete](./ask-event-doc-soft-delete.md) — sets the `deletedAt` this action filters on.
53
- - [defineEventDocSummary](../../../config/features/event-doc-summary.md) — declares the store (and its ordering index) this reads from.
97
+ - [askEventDocSoftDelete](./ask-event-doc-soft-delete.md) — sets the `deletedAt` these actions filter on.
98
+ - [askKeyValueStoreQuery](../../core/key-value-store/ask-key-value-store-query.md) — the underlying paged query action.
99
+ - [defineEventDocSummary](../../../config/features/event-doc-summary.md) — declares the store (and its ordering index) these read from.
@@ -17,10 +17,11 @@ This is the same pattern as [askContextProvideValue](../../core/context/ask-cont
17
17
  | `eventsStoreName` | `string` | The events-log store name — by convention `` `${storeName}Events` ``. |
18
18
  | `type` | `string` | Pins the document type within a store that can hold several. |
19
19
  | `storageDriveName` | `string` | The collection's blob bucket (assets + runtime artifacts), keyed per-doc. |
20
- | `eventValidator` | `string` (optional) | The collection's append-time validator inline-function name, if configured. |
21
- | `eventRenderer` | `string` (optional) | The collection's render inline-function name, if configured (powers `GET .../render`). |
20
+ | `onPublish` | `string` (optional) | The collection's on-publish inline-function name, if configured. Invoked after a Publish event is durably appended. |
21
+ | `onAppend` | `string` (optional) | The collection's on-append inline-function name, if configured. Invoked after every successful append. |
22
22
  | `scopeResolver` | `string` (optional) | The collection's ambient storage scope resolver inline-function name, if configured (e.g. per-tenant). |
23
- | `referenceResolver` | `string` (optional) | The collection's reference-collector inline-function name, if configured (powers `GET .../references` and the transfer feature's manifest walk). |
23
+
24
+ Render, references, and snapshot behaviour are no longer carried on the store binding — they come from the collection's registered `EventDocFunctions` object, addressed by `storeName`/`type` via `eventDocFunctionsName(storeName, type)` (see [defineEventDoc](../../../config/features/event-doc.md)).
24
25
 
25
26
  There are two ways to establish the context — one for custom routes, one for the built-in routes — plus the raw provide/read primitives and a resolver that throws when the binding is missing.
26
27
 
@@ -64,12 +65,9 @@ function* askEventDocProvideStore<T>(
64
65
  | --- | --- | --- |
65
66
  | `storeName` | `string` | The collection's record store name; the events-table and blob-drive names are derived from it. |
66
67
  | `type` | `string` | The document type pinned within the store. |
67
- | `eventValidator` | `string` (optional) | Append-time validator inline-function name. |
68
- | `eventRenderer` | `string` (optional) | Render inline-function name. |
69
68
  | `onPublish` | `string` (optional) | Inline-function name invoked after a Publish append. |
70
69
  | `onAppend` | `string` (optional) | Inline-function name invoked after every append. |
71
70
  | `scopeResolver` | `string` (optional) | Ambient storage scope resolver inline-function name. |
72
- | `referenceResolver` | `string` (optional) | Reference-collector inline-function name (powers `GET .../references`). |
73
71
 
74
72
  ### Returns
75
73
 
@@ -77,7 +75,7 @@ function* askEventDocProvideStore<T>(
77
75
 
78
76
  ## askEventDocProvideStoreFromGlobals
79
77
 
80
- The built-in-routes counterpart: bridges the per-route **globals** that `defineEventDocRoutes` sets (store name, events-store name, type, storage drive, and the optional validator/renderer) into the store context, then runs the controller sub-story. Reads each global with [askConfigGetGlobal](../../core/config/ask-config-get-global.md), which throws if a route forgot to set them.
78
+ The built-in-routes counterpart: bridges the per-route **globals** that `defineEventDocRoutes` sets (store name, events-store name, type, storage drive, and the optional onPublish/onAppend/scopeResolver hooks) into the store context, then runs the controller sub-story. Reads each global with [askConfigGetGlobal](../../core/config/ask-config-get-global.md), which throws if a route forgot to set them.
81
79
 
82
80
  ```typescript
83
81
  import { askEventDocProvideStoreFromGlobals } from 'quidproquo-features';
@@ -123,7 +121,7 @@ function* askEventDocStoreProvide<T>(
123
121
 
124
122
  ## askEventDocStoreRead
125
123
 
126
- The raw context **reader**: returns the currently bound [`EventDocStore`](#eventdocstore--the-binding). Built with `createContextReader`. Outside a provider it returns the **empty default** (blank `storeName` / `type`) rather than throwing — so most callers should prefer [askEventDocResolveStore](#askeventdocresolvestore). Internal data stories that only need one field (e.g. `eventValidator`) read it directly.
124
+ The raw context **reader**: returns the currently bound [`EventDocStore`](#eventdocstore--the-binding). Built with `createContextReader`. Outside a provider it returns the **empty default** (blank `storeName` / `type`) rather than throwing — so most callers should prefer [askEventDocResolveStore](#askeventdocresolvestore). Internal data stories that only need one field (e.g. `scopeResolver`) read it directly.
127
125
 
128
126
  ### Signature
129
127
 
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: askEventDocReferencesFromState
3
+ description: The other docs the CURRENT document depends on, one hop out, folded snapshot-seeded rather than walking the log.
4
+ ---
5
+
6
+ # askEventDocReferencesFromState
7
+
8
+ Reads the `EventDocLink`s the **current** document state depends on, one hop out. Folds the document snapshot-seeded (via [askEventDocDocumentStateLatest](./ask-event-doc-get-by-id.md)) and hands the folded state to the `collectReferencesFromState` member of the collection's registered `EventDocFunctions` object (looked up as `eventDocFunctionsName(storeName, type)`, see [defineEventDoc](../../../config/features/event-doc.md)). No log walk — this is the references ROUTE's read: what the document references now, at the cost of the gap since the nearest snapshot rather than the whole log.
9
+
10
+ A collection with no registered functions object is a leaf: this returns `[]`. So does a document with no events.
11
+
12
+ - **Built from:** [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) (to read the collection's `storeName`/`type`, which address its functions registration) and `askEventDocDocumentStateLatest` (the folded state `collectReferencesFromState` walks). Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
13
+
14
+ ```typescript
15
+ import { askEventDocReferencesFromState } from 'quidproquo-features';
16
+
17
+ export function* readTemplateDependencies(templateId: string) {
18
+ const links = yield* askEventDocReferencesFromState(templateId);
19
+
20
+ return links; // EventDocLink[] — e.g. the template's layout, styles, and content
21
+ }
22
+ ```
23
+
24
+ ## Signature
25
+
26
+ ```typescript
27
+ function* askEventDocReferencesFromState(docId: string): AskResponse<EventDocLink[]>;
28
+ ```
29
+
30
+ ## Parameters
31
+
32
+ | Parameter | Type | Description |
33
+ | --- | --- | --- |
34
+ | `docId` | `string` | The document to read outbound references for. |
35
+
36
+ ## Returns
37
+
38
+ `EventDocLink[]` — the doc's outbound links, one hop out, as of the current state. Empty when the collection has no registered `EventDocFunctions` object, that object's `collectReferencesFromState` returns none, or the document has no events.
39
+
40
+ ## Notes
41
+
42
+ - This is a **one-hop, current-state** read — the full-history equivalent (every link ANY historical state ever held) is [askEventDocReferences](./ask-event-doc-references.md), used by the transfer export instead.
43
+ - `GET {basePath}/{id}/references`, mounted by [defineEventDocRoutes](../../../config/features/event-doc-routes.md), calls this for one document; the route is always mounted, resolving to `[]` when no functions object is registered.
44
+
45
+ ## Related
46
+
47
+ - [defineEventDoc](../../../config/features/event-doc.md) — registers the `EventDocFunctions` object (`collectReferencesFromState`) this reads.
48
+ - [askEventDocReferences](./ask-event-doc-references.md) — the full-history sibling read, used by the transfer export.
49
+ - [askEventDocRenderForCollection](./ask-event-doc-render-for-collection.md) — the sibling read (`render`) on the same registered object.
50
+ - [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md) — read the document's own summary alongside its references.
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  title: askEventDocReferences
3
- description: The other docs one document depends on, one hop out, via its collection's referenceResolver.
3
+ description: Every doc one document has ever depended on, across its whole history, via its collection's registered EventDocFunctions object.
4
4
  ---
5
5
 
6
6
  # askEventDocReferences
7
7
 
8
- Reads the `EventDocLink`s a document depends on, one hop out. Hands the collection's `referenceResolver` inline function (see [defineEventDocRoutes](../../../config/features/event-doc-routes.md#parameters)) the document's whole event log and lets it fold + walk it. A collection with no resolver configured is a leaf: this returns `[]` without reading the log at all.
8
+ Reads the `EventDocLink`s a document has **ever** depended on, one hop out, across its whole history. Hands the document's whole event log to the `collectReferences` member of the collection's registered `EventDocFunctions` object (looked up as `eventDocFunctionsName(storeName, type)`, see [defineEventDoc](../../../config/features/event-doc.md)) and lets it fold + walk it, so a link that existed only in an older version is still found. A collection with no registered functions object is a leaf: this returns `[]`.
9
9
 
10
- - **Built from:** [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) (to read the collection's `referenceResolver` name) and `askEventDocEventListAll` (the full log the resolver folds). Requires the store context call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
10
+ This is the **transfer export's** read it exports the whole history and must chase links from all of it. `GET {basePath}/{id}/references` uses [askEventDocReferencesFromState](./ask-event-doc-references-from-state.md) instead (the CURRENT document only, folded snapshot-seeded rather than walking the log).
11
+
12
+ - **Built from:** [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) (to read the collection's `storeName`/`type`, which address its functions registration) and `askEventDocEventListAll` (the full log `collectReferences` folds). Requires the store context — call it inside `askEventDocProvideStore({ storeName, type }, ...)`, or from a built-in route where the context is already provided.
11
13
 
12
14
  ```typescript
13
15
  import { askEventDocReferences } from 'quidproquo-features';
@@ -33,15 +35,17 @@ function* askEventDocReferences(docId: string): AskResponse<EventDocLink[]>;
33
35
 
34
36
  ## Returns
35
37
 
36
- `EventDocLink[]` — the doc's outbound links, one hop out. Empty when the collection has no `referenceResolver` configured.
38
+ `EventDocLink[]` — the doc's outbound links, one hop out. Empty when the collection has no registered `EventDocFunctions` object, or that object's `collectReferences` returns none.
37
39
 
38
40
  ## Notes
39
41
 
40
- - This is a **one-hop** read. The recursive walk over these edges (following a template into its content, then that content's own references, and so on) is the transfer feature's job — see [askEventDocManifest](../event-doc-transfer/ask-event-doc-manifest.md).
41
- - `GET {basePath}/{id}/references`, mounted by [defineEventDocRoutes](../../../config/features/event-doc-routes.md), calls this for one document; the route is always mounted, resolving to `[]` when no `referenceResolver` is configured.
42
+ - This is a **one-hop** read, over the **whole log**. The recursive walk over these edges (following a template into its content, then that content's own references, and so on) is the transfer feature's job — see [askEventDocManifest](../event-doc-transfer/ask-event-doc-manifest.md).
43
+ - `GET {basePath}/{id}/references`, mounted by [defineEventDocRoutes](../../../config/features/event-doc-routes.md), calls [askEventDocReferencesFromState](./ask-event-doc-references-from-state.md) instead of this the route only ever needs the CURRENT document's references.
42
44
 
43
45
  ## Related
44
46
 
45
- - [defineEventDocRoutes](../../../config/features/event-doc-routes.md) — declares the `referenceResolver` this reads.
47
+ - [defineEventDoc](../../../config/features/event-doc.md) — registers the `EventDocFunctions` object (`collectReferences`) this reads.
48
+ - [askEventDocReferencesFromState](./ask-event-doc-references-from-state.md) — the sibling, current-state-only read backing the references route.
49
+ - [askEventDocRenderForCollection](./ask-event-doc-render-for-collection.md) — the sibling read (`render`) on the same registered object.
46
50
  - [askEventDocManifest](../event-doc-transfer/ask-event-doc-manifest.md) — the recursive walk built on top of this, one collection at a time.
47
51
  - [askEventDocGetByIdOrThrow](./ask-event-doc-get-by-id.md) — read the document's own summary alongside its references.
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: askEventDocRenderForCollection
3
+ description: Render a document from another collection in-process, addressed by identity alone.
4
+ ---
5
+
6
+ # askEventDocRenderForCollection
7
+
8
+ Renders a document of **another** collection in-process — the in-lambda twin of the `GET {basePath}/{id}/render` route, for cross-collection composition (a template resolving its layout or content links as HTML). Provides the target collection's store context so the renderer's own reads (blob-drive assets, linked docs) resolve against the right stores, then invokes the `render` member of that collection's registered `EventDocFunctions` object with the caller-supplied `input`.
9
+
10
+ Unlike the render route, nothing here is soft: the caller names a specific collection expecting a renderer, so a missing registration or a functions object with no `render` member propagates as its dynamic-functions error rather than a 404.
11
+
12
+ - **Built from:** `askEventDocProvideStore` (to bind the target collection's store context) and `createDynamicFunctionCaller<EventDocInvokableFunctions>` (to invoke `render` on `eventDocFunctionsName(storeName, type)`, see [defineEventDoc](../../../config/features/event-doc.md)). The caller resolves WHICH state to render first — this only renders the input it's handed.
13
+
14
+ ```typescript
15
+ import { askEventDocRenderForCollection } from 'quidproquo-features';
16
+
17
+ export function* renderTemplateLayout(state: unknown, docId: string) {
18
+ const result = yield* askEventDocRenderForCollection('layouts', 'layout', { state, docId });
19
+
20
+ return result.html;
21
+ }
22
+ ```
23
+
24
+ ## Signature
25
+
26
+ ```typescript
27
+ function* askEventDocRenderForCollection(
28
+ storeName: string,
29
+ type: string,
30
+ input: EventDocRenderInput,
31
+ ): AskResponse<EventDocRenderResult>;
32
+ ```
33
+
34
+ ## Parameters
35
+
36
+ | Parameter | Type | Description |
37
+ | --- | --- | --- |
38
+ | `storeName` | `string` | The target collection's store name (not necessarily the caller's own). |
39
+ | `type` | `string` | The target collection's document type. |
40
+ | `input` | `EventDocRenderInput` | `{ state, docId, version?, renderMode?, effectiveAt? }` — the already-folded document state to render, exactly as the render route builds it. |
41
+
42
+ ## Returns
43
+
44
+ `AskResponse<EventDocRenderResult>` — the rendered result (e.g. `{ kind: 'html', html }`).
45
+
46
+ ## Notes
47
+
48
+ - Requires the target collection to have a `render` member on its registered `EventDocFunctions` object; a collection with none throws the dynamic-functions "not found" error rather than resolving to an empty result.
49
+ - The caller is responsible for resolving `renderMode`/`effectiveAt` into a concrete, already-folded `state` before calling this — it renders exactly what it's handed, the same contract the render route's controller follows.
50
+
51
+ ## Related
52
+
53
+ - [defineEventDoc](../../../config/features/event-doc.md) — registers the `EventDocFunctions` object (`render`) this invokes.
54
+ - [askEventDocReferences](./ask-event-doc-references.md) — the sibling one-hop read (`collectReferences`) on the same registered object.
@@ -5,7 +5,7 @@ description: Walk one or more docs' references outward to find everything that h
5
5
 
6
6
  # askEventDocManifest
7
7
 
8
- Finds every doc that has to travel with a list of starting docs, by following each doc's `referenceResolver` links outward — breadth-first, across collections, with a visited set so a stylesheet three templates share is walked once and lands in the result once. Also the source of a cycle's termination: a link cycle (template → content → template) stops on the visited check instead of recursing forever.
8
+ Finds every doc that has to travel with a list of starting docs, by following each doc's `collectReferences` links outward — breadth-first, across collections, with a visited set so a stylesheet three templates share is walked once and lands in the result once. Also the source of a cycle's termination: a link cycle (template → content → template) stops on the visited check instead of recursing forever.
9
9
 
10
10
  Takes a **list** of roots so selecting several documents produces one merged manifest, rather than one per selection. A soft-deleted doc is reported (`deleted: true`) but not walked into — it will never be bundled, so its own dependencies are moot.
11
11
 
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: defineDynamicFunctions
3
+ description: Register a module's exported object of functions as a named dynamic-functions surface that other stories can invoke by member name with askDynamicFunctionExecute.
4
+ ---
5
+
6
+ # defineDynamicFunctions
7
+
8
+ Registers a module export — an **object whose properties are functions** — under a name, so any story can invoke one of its members by name with [askDynamicFunctionExecute](../../actions/core/dynamic-functions/ask-dynamic-function-execute.md) without a per-function registration. It is the successor to [defineInlineFunction](./inline-function.md): one setting addresses a whole surface (name + member) instead of one function per entry.
9
+
10
+ - **On AWS:** deploys **no dedicated infrastructure** of its own. The referenced module is loaded and its member is invoked inside whatever Lambda is already running the calling story, sharing its action processors and session context. Registration simply makes the surface resolvable by name.
11
+
12
+ ```typescript
13
+ import { defineDynamicFunctions } from 'quidproquo-core';
14
+
15
+ export default [
16
+ defineDynamicFunctions('templateEventDoc', '/entry/eventDocs::templateEventDoc'),
17
+ ];
18
+ ```
19
+
20
+ ## Signature
21
+
22
+ ```typescript
23
+ function defineDynamicFunctions(
24
+ dynamicFunctionsName: string,
25
+ runtime: QpqFunctionRuntime,
26
+ options?: QPQConfigAdvancedDynamicFunctionsSettings,
27
+ ): DynamicFunctionsQPQConfigSetting;
28
+ ```
29
+
30
+ ## Parameters
31
+
32
+ ### `dynamicFunctionsName` — `string` (required)
33
+
34
+ The name callers pass to [askDynamicFunctionExecute](../../actions/core/dynamic-functions/ask-dynamic-function-execute.md). This is the config's `uniqueKey`.
35
+
36
+ ### `runtime` — `QpqFunctionRuntime` (required)
37
+
38
+ A reference to the module to register, usually a relative path string of the form `'/path/to/file::exportedObjectName'`. The exported value must be an object whose own enumerable properties are functions — that object's members are the callable surface.
39
+
40
+ ### `options` — `QPQConfigAdvancedDynamicFunctionsSettings` (optional)
41
+
42
+ | Property | Type | Default | Description |
43
+ | --- | --- | --- | --- |
44
+ | `owner` | `CrossModuleOwner<'dynamicFunctionsName'>` | – | Declares that the functions object is owned by **another** module/service, so it can be resolved/invoked across modules. `{ module, application, feature, environment, dynamicFunctionsName }` — all optional; unset parts default to the current service. |
45
+ | `deprecated` | `boolean` | `false` | Marks the setting as deprecated in the config. |
46
+
47
+ ## Examples
48
+
49
+ ```typescript
50
+ import { defineDynamicFunctions } from 'quidproquo-core';
51
+
52
+ export default [
53
+ defineDynamicFunctions('templateEventDoc', '/entry/eventDocs::templateEventDoc'),
54
+
55
+ // Owned by another module
56
+ defineDynamicFunctions('billingHelpers', '/entry/functions/billing::billingHelpers', {
57
+ owner: { module: 'billing' },
58
+ }),
59
+ ];
60
+ ```
61
+
62
+ ## Related
63
+
64
+ - [askDynamicFunctionExecute](../../actions/core/dynamic-functions/ask-dynamic-function-execute.md) — invokes a member of a registered dynamic-functions object by name and returns its result.
65
+ - [defineInlineFunction](./inline-function.md) — registers a single story as a callable function; use this instead when the surface is one exported object with several members.
@@ -52,7 +52,7 @@ Each key is matched against a delivered message's `type` field. The key may be a
52
52
  | Property | Type | Default | Description |
53
53
  | --- | --- | --- | --- |
54
54
  | `batchSize` | `number` | `0` | Max number of messages delivered to the consumer Lambda per invocation. `0` leaves the SQS default. When set (> 0), the event source uses this batch size. |
55
- | `batchWindowInSeconds` | `number` | `5` | Max time SQS waits to fill a batch before invoking the consumer. Only applied when `batchSize > 0`, and **not** applied to FIFO queues (which don't support a batching window). |
55
+ | `batchWindowInSeconds` | `number` | `0` | Max time SQS waits to fill a batch before invoking the consumer — invoke as soon as a message arrives. Only applied when `batchSize > 0`, and **not** applied to FIFO queues (which don't support a batching window). A queue that wants to accumulate a batch before invoking should set this explicitly. |
56
56
  | `concurrency` | `number` | `1` | Consumer concurrency hint. |
57
57
  | `maxTries` | `number` | `1` | How many times a message is delivered before it is sent to the dead-letter queue (SQS `maxReceiveCount`). |
58
58
  | `ttRetryInSeconds` | `number` | `900` | Retry/visibility timeout in seconds — how long a message stays invisible while being processed before it can be redelivered. Also used as the consumer Lambda timeout. Capped at 900 (15 minutes). |
@@ -7,7 +7,7 @@ description: Declare the event document that records one audited session per adm
7
7
 
8
8
  Declares the **admin session event document** — the audit record behind the admin dashboard. It creates one event doc per admin login, appending every user-intent event for that session so operator activity in the dashboard is fully auditable.
9
9
 
10
- It is built on the quidproquo **eventDoc** feature (`defineEventDoc`): an event document is an append-only, per-instance record backed by storage, routes, and a WebSocket feed. This define fixes that eventDoc's store name, type, base path, and auth directory to the admin conventions, so you don't configure them yourself.
10
+ It is built on the quidproquo **eventDoc** feature an event document is an append-only, per-instance record backed by storage, routes, and a WebSocket feed — composed directly from [defineEventDocSummary](./event-doc-summary.md) and [defineEventDocRoutes](./event-doc-routes.md) rather than [defineEventDoc](./event-doc.md), since the session collection registers no functions object (no render, no references, no snapshots). This define fixes the store name, type, base path, and auth directory to the admin conventions, so you don't configure them yourself.
11
11
 
12
12
  `defineAdminSessionEventDoc` returns a `QPQConfig` array. It is spread in automatically by [defineAdminSettings](./admin-settings.md), so you rarely call it directly — but it is exported for services that assemble the admin feature piece by piece.
13
13
 
@@ -5,7 +5,7 @@ description: Mount the built-in HTTP routes for an event-sourced document collec
5
5
 
6
6
  # defineEventDocRoutes
7
7
 
8
- Mounts the built-in REST **routes** for one document `type` in an event-document collection. It returns a `QPQConfig` (an array of route settings). The route controllers ship inside `quidproquo-features` and resolve the store, type, user directory, validator, and renderer from per-route globals — so a service needs no controller wiring of its own: declare the store, add these routes, and the collection is fully served.
8
+ Mounts the built-in REST **routes** for one document `type` in an event-document collection. It returns a `QPQConfig` (an array of route settings). The route controllers ship inside `quidproquo-features` and resolve the store, type, and user directory from per-route globals, invoking the collection's registered `EventDocFunctions` object (looked up by convention as `eventDocFunctionsName(storeName, type)`, see [defineEventDoc](./event-doc.md)) for render/references/snapshot behaviour — so a service needs no controller wiring of its own: declare the store, register the functions object, add these routes, and the collection is fully served.
9
9
 
10
10
  `defineEventDocRoutes` defines **only** the routes; it assumes the store already exists (declared with [defineEventDocSummary](./event-doc-summary.md)). Use it directly when **several document types share one store** — call `defineEventDocSummary` once, then `defineEventDocRoutes` per type. For the single-type case, [defineEventDoc](./event-doc.md) does both in one call.
11
11
 
@@ -31,11 +31,11 @@ All paths are prefixed with the version segment `/v{version}` (default `/v1`):
31
31
 
32
32
  | Method | Path | Purpose |
33
33
  | --- | --- | --- |
34
- | `GET` | `{basePath}` | List the collection's documents. |
34
+ | `GET` | `{basePath}` | List one page of the collection's documents (newest first). Accepts `?limit=` and `?nextPageKey=` and returns `QpqPagedData`. |
35
35
  | `GET` | `{basePath}/{id}` | Get one document's summary record. |
36
- | `GET` | `{basePath}/{id}/events` | List a document's event log. |
37
- | `GET` | `{basePath}/{id}/render` | Render the document to HTML. **Only mounted when `eventRenderer` is set.** |
38
- | `GET` | `{basePath}/{id}/references` | List the `EventDocLink`s this document depends on. |
36
+ | `GET` | `{basePath}/{id}/events` | List a document's event log. Accepts `?limit=`, `?nextPageKey=`, `?afterEventId=` (exclusive — the tail since a known event), and `?newestFirst=true` (walk the log backwards — the history panel's latest-page-then-load-older read). With `?includeBase=true` the response becomes the bootstrap shape: `{ base, items, nextPageKey }`, where `base` is the newest usable document-view snapshot (`{ eventId, state }`, era-pinned) and `items` starts after it — or `base: null` with the log from event zero when no usable snapshot exists, so the fallback needs no second request shape. The base rides the first page only; page the rest with `?afterEventId=base.eventId`. |
37
+ | `GET` | `{basePath}/{id}/render` | Render the document. Resolves the target state snapshot-seeded (draft = the log's head, `?renderMode=published` = the version effective at `?effectiveAt=`/now) and hands the registered `render` member `{ state, docId, version?, ... }` — a render never replays a whole log. 404s as "no renderer configured" when the collection's registered functions object has no `render` member. |
38
+ | `GET` | `{basePath}/{id}/references` | List the `EventDocLink`s the CURRENT document state depends on (walks the folded state, not the log; the transfer export keeps the full-history walk). Resolves to `[]` when the collection has no registered functions object. |
39
39
  | `POST` | `{basePath}` | Create a document. |
40
40
  | `POST` | `{basePath}/{id}/events` | Append an event to a document. |
41
41
  | `POST` | `{basePath}/{id}/assets` | Request an asset upload URL. |
@@ -60,14 +60,13 @@ The single `options` argument is an `EventDocRoutesOptions`:
60
60
  | `basePath` | `` `/${string}` `` | – (required) | URL prefix the routes mount under. Must start with `/`, e.g. `/articles`. |
61
61
  | `routeAuthSettings` | `RouteAuthSettings` | – | Auth settings applied to every mounted route (from quidproquo-webserver — see [route](../webserver/route.md)). When it carries a `userDirectoryName`, that directory is exposed to the controllers so mutations can attribute the acting user. **Omit to leave the routes open** — mutations then have no user to attribute. |
62
62
  | `version` | `number` | `1` | Version number for the `/v{version}` path prefix on every route. |
63
- | `eventValidator` | `string` | – | Name of a registered inline function (see `defineInlineFunction`). When set, every append invokes it with `{ event, events }` to reject lifecycle- or payload-invalid events before they reach the log. The frontend editor runs the same rule for instant feedback. |
64
- | `eventRenderer` | `string` | – | Name of a registered inline function. When set, a `GET {basePath}/{id}/render` route is mounted; it invokes the renderer with the document's full `{ events }` log, which folds + renders to HTML. |
65
- | `onPublish` | `string` | – | Name of a registered inline function. When set, every successful append of a Publish event invokes it with `{ docId, event, summary }`, after the event is durably written and the summary re-derived. This is the seam for syncing a folded document into a materialized read model. Errors propagate to the caller: the event has landed but the side effect did not, so the caller learns the read model may be stale. |
66
- | `onAppend` | `string` | – | Name of a registered inline function. When set, EVERY successful append (domain events and lifecycle events alike) invokes it with `{ docId, event, summary, events }`, after the event is durably written and the summary re-derived. This is the seam for reacting to any mutation (e.g. broadcasting the doc's fresh fold). Runs after `onPublish` when both fire on the same Publish event. Errors propagate to the caller: the event has landed but the side effect did not. |
63
+ | `onPublish` | `string` | – | Name of a registered inline function. When set, every successful append of a Publish event invokes it with `{ docId, event, summary, state, previousState }` the FOLDED document as of the publish event and as of the event before it (latest-shaped, snapshot-seeded, so hook cost tracks the burst since the last snapshot, never the log) — after the event is durably written. This is the seam for syncing a folded document into a materialized read model: use `state`, never re-fold. Errors propagate to the caller: the event has landed but the side effect did not, so the caller learns the read model may be stale. |
64
+ | `onAppend` | `string` | – | Name of a registered inline function. When set, EVERY successful append (domain events and lifecycle events alike) invokes it with `{ docId, event, summary, state, previousState }` the FOLDED document as of the event and as of its predecessor (latest-shaped, snapshot-seeded) — after the event is durably written. This is the seam for reacting to any mutation (e.g. broadcasting the doc's fresh fold, or diffing `previousState` vs `state` to detect a transition). Runs after `onPublish` when both fire on the same Publish event. Errors propagate to the caller: the event has landed but the side effect did not. |
67
65
  | `scopeResolver` | `string` | – | Name of a registered inline function. When set, every route invokes it with `{ event }` before running; a non-null result becomes the ambient storage scope for the whole request, transparently partitioning the collection's stores and assets (e.g. per-tenant). Null means unscoped. Omit for collections that never partition. |
68
- | `referenceResolver` | `string` | – | Name of a registered inline function. When set, `GET {basePath}/{id}/references` invokes it with the document's `{ events, docId }` log, which folds and returns the `EventDocLink`s that view depends on (e.g. a template's layout/style/content links). The transfer feature's manifest walk follows those links recursively. Omit for a leaf collection (a stylesheet, a layout) — the route is still mounted but always resolves to `[]`. |
69
66
  | `excludeRoutes` | `EventDocRouteName[]` | `[]` | Route names to leave unmounted (`'list' \| 'get' \| 'listEvents' \| 'render' \| 'references' \| 'create' \| 'appendEvent' \| 'createAsset' \| 'getAsset' \| 'listAssets' \| 'remove'`). For a collection that must own one of these itself instead of using the stock behavior — e.g. a `create` that must also perform some side effect the stock controller doesn't know about. |
70
67
 
68
+ Append-time validation, rendering, references, and snapshotting are no longer per-route options: they come from the collection's registered `EventDocFunctions` object (`collectReferences`, `render`, `foldSnapshotViews`, and the doc type's own fold-gate `validators`), addressed by `eventDocFunctionsName(storeName, type)`. Register it with [defineDynamicFunctions](../core/dynamic-functions.md) directly (a multi-type store calling `defineEventDocRoutes` per type) or let [defineEventDoc](./event-doc.md) do it for the single-type case.
69
+
71
70
  ### `RouteAuthSettings`
72
71
 
73
72
  `routeAuthSettings` is the standard quidproquo-webserver route auth object (the same one [defineRoute](../webserver/route.md) accepts). Its `userDirectoryName` names the [user directory](../core/key-value-store.md) callers authenticate against; the controllers read it to resolve the acting user for event attribution.
@@ -75,19 +74,20 @@ The single `options` argument is an `EventDocRoutesOptions`:
75
74
  ## Examples
76
75
 
77
76
  ```typescript
78
- import { defineEventDocSummary, defineEventDocRoutes } from 'quidproquo-features';
77
+ import { defineDynamicFunctions } from 'quidproquo-core';
78
+ import { defineEventDocSummary, defineEventDocRoutes, eventDocFunctionsName } from 'quidproquo-features';
79
79
 
80
80
  export default [
81
81
  // One store, two types, each on its own path and route version.
82
- ...defineEventDocSummary('content'),
82
+ ...defineEventDocSummary('content', { snapshotFunctions: { article: eventDocFunctionsName('content', 'article') } }),
83
+
84
+ defineDynamicFunctions(eventDocFunctionsName('content', 'article'), '/entry/eventDocs::articleDefinition'),
83
85
 
84
86
  ...defineEventDocRoutes({
85
87
  storeName: 'content',
86
88
  type: 'article',
87
89
  basePath: '/articles',
88
90
  routeAuthSettings: { userDirectoryName: 'editors' },
89
- eventValidator: 'validateArticleEvent',
90
- eventRenderer: 'renderArticle',
91
91
  }),
92
92
 
93
93
  ...defineEventDocRoutes({
@@ -102,7 +102,8 @@ export default [
102
102
  ## Related
103
103
 
104
104
  - [defineEventDocSummary](./event-doc-summary.md) — declares the store these routes serve (required before mounting routes).
105
- - [defineEventDoc](./event-doc.md) — declares the store *and* these routes in one call for the single-type case.
106
- - [defineEventDocTransfer](./event-doc-transfer.md) — mounts the export/import routes that read every registered collection's `referenceResolver` to build a transfer manifest.
105
+ - [defineEventDoc](./event-doc.md) — declares the store, registers the functions object, *and* mounts these routes in one call for the single-type case.
106
+ - [defineDynamicFunctions](../core/dynamic-functions.md) — registers the `EventDocFunctions` object these routes invoke for render/references/snapshot behaviour.
107
+ - [defineEventDocTransfer](./event-doc-transfer.md) — mounts the export/import routes that read every registered collection's functions object (`collectReferences`) to build a transfer manifest.
107
108
  - [defineRoute](../webserver/route.md) — the underlying webserver route config (source of `RouteAuthSettings`).
108
109
  - **Custom routes over the same store:** wrap your controllers in `askEventDocProvideStore({ storeName, type }, ...)` (from quidproquo-features) so the generic [reads](../../actions/features/event-doc/ask-event-doc-get-by-id.md) resolve the collection, then compose actions like [askEventDocCreate](../../actions/features/event-doc/ask-event-doc-create.md).
@@ -5,16 +5,17 @@ description: Declare the stores for an event-sourced document collection — its
5
5
 
6
6
  # defineEventDocSummary
7
7
 
8
- Declares the **stores** that back an event-document collection, without any routes. It returns a `QPQConfig` (an array of config settings) that expands to four underlying core stores:
8
+ Declares the **stores** that back an event-document collection, without any routes. It returns a `QPQConfig` (an array of config settings) that expands to five underlying core stores:
9
9
 
10
10
  1. A **summary key-value store** (partition key `type`, sort key `id`) — the queryable record for each document, derived by folding the identity/lifecycle events of its log. Rows are the [`EventDocSummary`](../../actions/features/event-doc/ask-event-doc-get-by-id.md) shape and carry the version history. A secondary index on `(type, updatedAt)` supports the recently-updated ordering [askEventDocList](../../actions/features/event-doc/ask-event-doc-list.md) returns. It is a pure projection: nothing on the append path writes it directly. The events store below declares an [`onStream`](../core/key-value-store.md#change-data-capture-onstream) handler that rebuilds a document's summary row from its log whenever an event is appended, so the summary is eventually (not immediately) consistent with the log.
11
11
  2. An **append-only events store** (`<storeName>EventLog`, partition key `pk`, string sort key `sk`) — the live ordered log every document is folded from, keyed on a sortable event id (UUIDv7). It has **no** secondary index on purpose: the local dev-server query processor can't target one, so all event reads go through the main table.
12
- 3. A **legacy events store** (`<storeName>Events`, partition key `pk`, numeric sort key `sk`) — the pre-sortable-id log, kept declared but unread/unwritten at runtime so its data stays reachable until it is migrated into the events store above.
13
- 4. A **storage drive** (`<storeName>edocs`, lower-cased) — the collection's blob bucket, holding each document's immutable uploaded assets (and later its derived runtime artifacts) under per-document prefixes.
12
+ 3. A **snapshots store** (`<storeName>SS`, partition key `pk`, sort key `sk`) — per-view folded states at points along the log (`pk = docId#view`, `sk` the same sortable event id the log is ordered by). Populated only for the document types you enable via `options.snapshotFunctions`; a type with no entry there simply is not snapshotted. For a type with a registered functions object, the `onStream` handler runs ONE incremental fold per delivery resuming from the newest usable snapshot and folding only the gap since it and persists both the snapshot set and the summary row from that same fold (the fold's `summary` view IS the queryable record). The whole-log summary re-derivation remains only as the fallback: types with no registered functions, Remove stream records (a transfer rewrote the log, so snapshots can't seed), and folds that decline.
13
+ 4. A **legacy events store** (`<storeName>Events`, partition key `pk`, numeric sort key `sk`) — the pre-sortable-id log, kept declared but unread/unwritten at runtime so its data stays reachable until it is migrated into the events store above.
14
+ 5. A **storage drive** (`<storeName>edocs`, lower-cased) — the collection's blob bucket, holding each document's immutable uploaded assets (and later its derived runtime artifacts) under per-document prefixes.
14
15
 
15
- Point-in-time recovery is enabled on all three tables.
16
+ Point-in-time recovery is enabled on all tables.
16
17
 
17
- - **On AWS:** deploys three DynamoDB tables (via [defineKeyValueStore](../core/key-value-store.md)) and one S3 bucket (via [defineStorageDrive](../core/storage-drive.md)). All physical names are derived from `keyValueStoreName`, so a collection needs only that one name.
18
+ - **On AWS:** deploys four DynamoDB tables (via [defineKeyValueStore](../core/key-value-store.md)) and one S3 bucket (via [defineStorageDrive](../core/storage-drive.md)). All physical names are derived from `keyValueStoreName`, so a collection needs only that one name.
18
19
 
19
20
  ```typescript
20
21
  import { defineEventDocSummary } from 'quidproquo-features';
@@ -29,7 +30,7 @@ Use `defineEventDocSummary` when you want to define the store separately from th
29
30
  ## Signature
30
31
 
31
32
  ```typescript
32
- function defineEventDocSummary(keyValueStoreName: string): QPQConfig;
33
+ function defineEventDocSummary(keyValueStoreName: string, options?: EventDocSummaryOptions): QPQConfig;
33
34
  ```
34
35
 
35
36
  ## Parameters
@@ -38,6 +39,12 @@ function defineEventDocSummary(keyValueStoreName: string): QPQConfig;
38
39
 
39
40
  The collection's base store name. It is used directly as the summary store name and as the `storeName` that route definitions and store-context calls reference. The events table name (`` `${name}Events` ``) and asset bucket name (`` `${name}edocs`.toLowerCase() ``) are both derived from it, so the whole collection is addressed by this single name. It must match the `storeName` passed to any [defineEventDocRoutes](./event-doc-routes.md) (or [askEventDocProvideStore](#related)) for the same collection.
40
41
 
42
+ ### `options` — `EventDocSummaryOptions` (optional)
43
+
44
+ | Property | Type | Default | Description |
45
+ | --- | --- | --- | --- |
46
+ | `snapshotFunctions` | `Record<string, string>` | `{}` | Registered dynamic-functions names (see [defineDynamicFunctions](../core/dynamic-functions.md)) of each collection's `EventDocFunctions` object, keyed by document `type` — keyed because one events table can host several collections and its one stream serves them all. When a row's type has an entry, the stream projector invokes that object's `foldSnapshotViews` with the doc's log prefix (or, once a usable snapshot exists, just the gap since it, plus that snapshot's per-view state as `seedViews`) and writes the folded views to the snapshots store; a type with no entry is not snapshotted. [defineEventDoc](./event-doc.md) threads the single-type case through automatically — call this directly only for a multi-type store. |
47
+
41
48
  ## Examples
42
49
 
43
50
  ```typescript
@@ -52,6 +59,19 @@ export default [
52
59
  ];
53
60
  ```
54
61
 
62
+ ```typescript
63
+ import { defineDynamicFunctions } from 'quidproquo-core';
64
+ import { defineEventDocRoutes, defineEventDocSummary, eventDocFunctionsName } from 'quidproquo-features';
65
+
66
+ // A shared store where "article" documents are snapshotted and "page" documents are not.
67
+ export default [
68
+ ...defineEventDocSummary('content', { snapshotFunctions: { article: eventDocFunctionsName('content', 'article') } }),
69
+ defineDynamicFunctions(eventDocFunctionsName('content', 'article'), '/entry/eventDocs::articleDefinition'),
70
+ ...defineEventDocRoutes({ storeName: 'content', type: 'article', basePath: '/articles' }),
71
+ ...defineEventDocRoutes({ storeName: 'content', type: 'page', basePath: '/pages' }),
72
+ ];
73
+ ```
74
+
55
75
  ## Related
56
76
 
57
77
  - [defineEventDocRoutes](./event-doc-routes.md) — mounts the HTTP routes against a store declared here (one per type).
@@ -7,7 +7,7 @@ description: Mount export/import routes that move event-doc collections between
7
7
 
8
8
  Mounts the export/import **routes** for a service's event-doc collections: a staging storage drive plus five routes that build, stage, plan, and apply portable bundles of one or more documents (and everything they reference). It returns a `QPQConfig` (an array of config settings) that you spread into a service's infrastructure default export, alongside its `defineEventDoc` calls.
9
9
 
10
- An export walks a doc's `referenceResolver` (see [defineEventDocRoutes](./event-doc-routes.md#parameters)) to pull in every doc it depends on — a template's layout, styles, and content — so a bundle is self-contained. An import replays that bundle's event log against the target's own collections, either as a fresh doc or appended onto one that already exists there, and reports what it would do (or did) per document.
10
+ An export walks a doc's registered functions object `collectReferences` (see [defineEventDoc](./event-doc.md)) to pull in every doc it depends on — a template's layout, styles, and content — so a bundle is self-contained. An import replays that bundle's event log against the target's own collections, either as a fresh doc or appended onto one that already exists there, and reports what it would do (or did) per document.
11
11
 
12
12
  The route controllers ship inside `quidproquo-features` and read the collection registry from per-route globals, so a service needs no controller wiring of its own.
13
13
 
@@ -15,15 +15,14 @@ The route controllers ship inside `quidproquo-features` and read the collection
15
15
 
16
16
  ```typescript
17
17
  import { defineEventDoc, defineEventDocTransfer } from 'quidproquo-features';
18
+ import { articleDefinition } from './articleDefinition';
19
+ import { templateDefinition } from './templateDefinition';
18
20
 
19
- const collections = [
20
- { storeName: 'templates', type: 'template', referenceResolver: 'collectTemplateReferences' },
21
- { storeName: 'content', type: 'article' },
22
- ];
21
+ const collections = [templateDefinition, articleDefinition];
23
22
 
24
23
  export default [
25
- ...defineEventDoc({ storeName: 'templates', type: 'template', basePath: '/templates', referenceResolver: 'collectTemplateReferences' }),
26
- ...defineEventDoc({ storeName: 'content', type: 'article', basePath: '/articles' }),
24
+ ...defineEventDoc(templateDefinition, '/entry/eventDocs::templateDefinition', { basePath: '/templates' }),
25
+ ...defineEventDoc(articleDefinition, '/entry/eventDocs::articleDefinition', { basePath: '/articles' }),
27
26
 
28
27
  ...defineEventDocTransfer({
29
28
  service: 'cms',
@@ -39,7 +38,7 @@ All paths are prefixed with the version segment `/v{version}` (default `/v1`):
39
38
 
40
39
  | Method | Path | Purpose |
41
40
  | --- | --- | --- |
42
- | `POST` | `/transfer/manifest` | Every doc that would travel with a given `{ docs: [{ service, type, id }, ...] }` selection, merged and deduped across all of them (follows `referenceResolver` links), without building anything. Feeds an export UI's "these will be included" preview. |
41
+ | `POST` | `/transfer/manifest` | Every doc that would travel with a given `{ docs: [{ service, type, id }, ...] }` selection, merged and deduped across all of them (follows each collection's `collectReferences` links), without building anything. Feeds an export UI's "these will be included" preview. |
43
42
  | `POST` | `/transfer/export` | Stage one bundle for `{ docs: [...] }` and return its download url. |
44
43
  | `POST` | `/transfer/upload` | A presigned PUT url for uploading a bundle file, plus the id used to plan/import it. |
45
44
  | `POST` | `/transfer/plan` | What importing an uploaded bundle (`{ transferId }`) would do to each of its docs (`new`, `fastForward`, `same`, or a blocking `diverged`/`codeConflict`) without writing anything. |
@@ -58,24 +57,22 @@ The single `options` argument is an `EventDocTransferOptions`:
58
57
  | Property | Type | Default | Description |
59
58
  | --- | --- | --- | --- |
60
59
  | `service` | `string` | – (required) | The service name `EventDocLink`s use to address this service's collections (`link.eventDocService`). Transfers never leave it: the stores live here, so a reference into another service throws rather than being silently dropped from a manifest. |
61
- | `collections` | `EventDocTransferCollection[]` | – (required) | The collections a transfer may read and write. Feed this the same array the service maps over for its `defineEventDoc` calls, so the two cannot drift; the extra fields on a routes-options object (`basePath`, auth, `version`) are simply unused here. |
60
+ | `collections` | `EventDocTransferCollectionSource[]` | – (required) | The collections a transfer may read and write. Feed this the same array the service maps into its `defineEventDoc` calls (each entry's functions object carries the identity), so the two cannot drift see `EventDocTransferCollectionSource` below. |
62
61
  | `scopeResolver` | `string` | – | Name of a registered inline function. Establishes the ambient storage scope for the whole request, exactly like a collection's own `scopeResolver`. Needed separately because a transfer spans collections, so there is no single store to read the resolver name from. Omit only if none of the collections partition. |
63
62
  | `routeAuthSettings` | `RouteAuthSettings` | – | Auth settings applied to every mounted route (from quidproquo-webserver — see [route](../webserver/route.md)). Import writes unvalidated history and export reads across every registered collection, so gate these harder than the collections' own routes. |
64
63
  | `version` | `number` | `1` | Version number for the `/v{version}` path prefix on every route. |
65
64
 
66
- ### `EventDocTransferCollection`
65
+ ### `EventDocTransferCollectionSource`
67
66
 
68
- A subset of a collection's own `EventDocRoutesOptions`, so a service can declare its collections once and feed the same array to both `defineEventDoc` and `defineEventDocTransfer`:
67
+ Each `collections` entry can be any of:
69
68
 
70
- | Property | Type | Description |
71
- | --- | --- | --- |
72
- | `storeName` | `string` | Must match the collection's own `storeName`. |
73
- | `type` | `string` | Must match the collection's own `type`. |
74
- | `onPublish` | `string` | Carried so an imported publish behaves exactly as it does on the collection's own routes. |
75
- | `onAppend` | `string` | Carried so an imported append fires the same reaction as it does on the collection's own routes. |
76
- | `referenceResolver` | `string` | Carried so the manifest walk can follow this collection's links. `eventValidator` is deliberately **not** carried — replayed history was already validated at its origin, so imports skip it. |
69
+ - An `EventDocFunctions` object the same value passed as `defineEventDoc`'s `functions` argument. Its `collectReferences` drives the manifest walk; `storeName`/`type` are read off it.
70
+ - A collection-list entry carrying its functions object under `functions` (`{ functions, ... }`) — so the exact array a service maps over for its `defineEventDoc` calls passes straight through, unmapped.
71
+ - A bare `EventDocTransferCollection` registry entry (`{ storeName, type, onPublish?, onAppend? }`), for a collection that needs the import hooks carried explicitly without passing a live functions object.
72
+
73
+ `onPublish`/`onAppend` are carried so an imported publish/append behaves exactly as it does on the collection's own routes. Fold-gate validation is deliberately **not** applied to imported events — replayed history was already validated at its origin.
77
74
 
78
75
  ## Related
79
76
 
80
- - [defineEventDoc](./event-doc.md) / [defineEventDocRoutes](./event-doc-routes.md) — declare the collections a transfer reads and writes, including the `referenceResolver` the manifest walk follows.
77
+ - [defineEventDoc](./event-doc.md) / [defineEventDocRoutes](./event-doc-routes.md) — declare the collections a transfer reads and writes, including the functions object (`collectReferences`) the manifest walk follows.
81
78
  - [defineTenantedEventDocTransfer](./tenanted-event-doc-transfer.md) — the same routes with the tenant scope resolver pre-wired, so export/import never cross tenant partitions.
@@ -1,71 +1,85 @@
1
1
  ---
2
2
  title: defineEventDoc
3
- description: Define an event-sourced document collection its stores plus its HTTP routes in one call.
3
+ description: Register an event-sourced document collection's functions object, declare its stores, and mount its HTTP routes, in one call.
4
4
  ---
5
5
 
6
6
  # defineEventDoc
7
7
 
8
- Defines a complete **event-document collection**: the data stores that hold it *and* the HTTP routes that serve it, in a single call. This is the all-in-one helper for the common **one-store-one-type** case. It returns a `QPQConfig` (an array of config settings) that you spread into a service's infrastructure default export.
8
+ Registers a document collection's **functions object** (its identity plus its behaviour — fold, references, and optionally render — as a [dynamic-functions](../core/dynamic-functions.md) surface), declares the stores that back it, and mounts the HTTP routes that serve it, all in one call. This is the all-in-one helper for the common **one-store-one-type** case. It returns a `QPQConfig` (an array of config settings) that you spread into a service's infrastructure default export.
9
9
 
10
- An event document is not stored as a mutable row. It is derived by folding an ordered, append-only log of events. `defineEventDoc` provisions that log (plus a queryable summary table and a blob bucket) and mounts the REST routes that create documents, append events, read them back, and resolve their draft/published versions.
10
+ An event document is not stored as a mutable row. It is derived by folding an ordered, append-only log of events. `defineEventDoc` provisions that log (plus a queryable summary table and a blob bucket), registers the collection's functions object so the backend can invoke it by name, and mounts the REST routes that create documents, append events, read them back, and resolve their draft/published versions.
11
11
 
12
- - **On AWS:** deploys everything [defineEventDocSummary](./event-doc-summary.md) deploys (two DynamoDB tables a summary table and an append-only events table — plus an S3 bucket for assets) and everything [defineEventDocRoutes](./event-doc-routes.md) deploys (the API Gateway routes and their Lambda handlers). It creates no infrastructure of its own; it is exactly `[defineEventDocSummary(options.storeName), defineEventDocRoutes(options)]`.
12
+ - **On AWS:** deploys everything [defineEventDocSummary](./event-doc-summary.md) deploys (DynamoDB tables for the summary, events, and snapshots, plus an S3 bucket for assets), everything [defineEventDocRoutes](./event-doc-routes.md) deploys (the API Gateway routes and their Lambda handlers), and a [defineDynamicFunctions](../core/dynamic-functions.md) registration for `functions` — no infrastructure of its own. It reads `functions`' `storeName`/`type` off the object itself, so `options` carries no identity fields.
13
13
 
14
14
  ```typescript
15
15
  import { defineEventDoc } from 'quidproquo-features';
16
+ import { articleDefinition } from './articleDefinition';
16
17
 
17
18
  export default [
18
- ...defineEventDoc({
19
- storeName: 'content',
20
- type: 'article',
19
+ ...defineEventDoc(articleDefinition, '/entry/eventDocs::articleDefinition', {
21
20
  basePath: '/articles',
22
21
  routeAuthSettings: { userDirectoryName: 'editors' },
23
22
  }),
24
23
  ];
25
24
  ```
26
25
 
26
+ ## Registering a collection's functions
27
+
28
+ `functions` is an `EventDocFunctions` object: `{ storeName, type, foldSnapshotViews, collectReferences, render? }`. The object `createEventDocDefinition` returns — given `storeName`/`type` in its config — satisfies this shape directly, so a collection with no service-only render can register its definition verbatim. A collection that needs a render step only service code can perform (resolving linked docs, reading blob-drive assets) layers it on with `extendEventDocFunctions(definition, { render })`, which returns a new object and never mutates the definition itself.
29
+
30
+ `runtime` is a [`QpqFunctionRuntime`](../core/dynamic-functions.md#runtime--qpqfunctionruntime-required) path to that SAME export — the dynamic-functions pattern: identity is read off the object here at config time, behaviour is loaded from the path by the processors at request time. Both must point at the exact object being registered, or the registration and the runtime will disagree about what the collection can do.
31
+
27
32
  ## When to use `defineEventDoc` vs `defineEventDocSummary` + `defineEventDocRoutes`
28
33
 
29
- `defineEventDoc` defines the store **and** the routes together, so it assumes exactly one document `type` per store. If you want several document types to share one physical store (one summary table, one events table, one bucket), you must not define the store more than once. In that case, call [defineEventDocSummary](./event-doc-summary.md) **once** for the shared store, then [defineEventDocRoutes](./event-doc-routes.md) **per type** — each with the same `storeName` but a different `type` and `basePath`. Use `defineEventDoc` whenever the store backs a single type.
34
+ `defineEventDoc` defines the store, the functions registration, **and** the routes together, so it assumes exactly one document `type` per store. If you want several document types to share one physical store (one summary table, one events table, one bucket), you must not define the store more than once. In that case, call [defineEventDocSummary](./event-doc-summary.md) **once** for the shared store, then [defineDynamicFunctions](../core/dynamic-functions.md) and [defineEventDocRoutes](./event-doc-routes.md) **per type** — each with the same `storeName` but a different `type`, `basePath`, and functions object, passing `defineEventDocSummary` the whole `snapshotFunctions` map. Use `defineEventDoc` whenever the store backs a single type.
35
+
36
+ A collection with no definition at all (no render, no references, no snapshots) composes the low-level pair directly instead of calling `defineEventDoc`: `[...defineEventDocSummary(storeName), ...defineEventDocRoutes({ storeName, type, ...options })]`.
30
37
 
31
38
  ## Signature
32
39
 
33
40
  ```typescript
34
- function defineEventDoc(options: EventDocRoutesOptions): QPQConfig;
41
+ function defineEventDoc(
42
+ functions: EventDocFunctions,
43
+ runtime: QpqFunctionRuntime,
44
+ options: EventDocCollectionOptions,
45
+ ): QPQConfig;
35
46
  ```
36
47
 
37
48
  ## Parameters
38
49
 
39
- `defineEventDoc` takes the same `EventDocRoutesOptions` object as [defineEventDocRoutes](./event-doc-routes.md); `options.storeName` is also passed straight through to [defineEventDocSummary](./event-doc-summary.md). See the [defineEventDocRoutes parameter reference](./event-doc-routes.md#parameters) for every field.
50
+ ### `functions` `EventDocFunctions` (required)
51
+
52
+ The collection's callable surface, read for its identity (`storeName`, `type`) at config time and registered under `runtime` for invocation at request time.
40
53
 
41
54
  | Property | Type | Required | Description |
42
55
  | --- | --- | --- | --- |
43
56
  | `storeName` | `string` | yes | Name of the summary store to create and serve. Also derives the events table and asset bucket names. |
44
57
  | `type` | `string` | yes | The document type this collection holds — the store's partition value, so one store can (via the split helpers) hold several types. |
45
- | `basePath` | `` `/${string}` `` | yes | URL prefix the routes mount under, e.g. `/articles`. |
46
- | `routeAuthSettings` | `RouteAuthSettings` | no | Auth for the mounted routes. Omit to leave them open mutations then have no user to attribute. |
47
- | `version` | `number` | no | Route version prefix (`/v{version}`), default `1`. |
48
- | `eventValidator` | `string` | no | Inline-function name run on every append to reject invalid events. |
49
- | `eventRenderer` | `string` | no | Inline-function name that folds + renders the log to HTML; mounting it adds a `GET {basePath}/{id}/render` route. |
50
- | `onPublish` | `string` | no | Inline-function name invoked with `{ docId, event, summary }` after every successful Publish append: the seam for syncing the folded document into a materialized read model. |
51
- | `onAppend` | `string` | no | Inline-function name invoked with `{ docId, event, summary, events }` after EVERY successful append (domain events and lifecycle events alike): the seam for reacting to any mutation. Runs after `onPublish` when both fire on the same Publish event. |
52
- | `scopeResolver` | `string` | no | Inline-function name every route invokes with `{ event }` to resolve the request's ambient storage scope (e.g. per-tenant); null means unscoped. |
53
- | `referenceResolver` | `string` | no | Inline-function name `GET {basePath}/{id}/references` invokes with the document's `{ events, docId }` log to return the `EventDocLink`s that view depends on. Omit for a leaf collection — the route still resolves, just always to `[]`. |
58
+ | `foldSnapshotViews` | `(events, seedViews?) => Nullable<EventDocSnapshotViews>` | yes | Every view of a log prefix, era-pinned — what a snapshot stores. Invoked by the event store's stream projector (which also writes the summary row from the fold's `summary` view). |
59
+ | `foldDocumentState` | `(events, seedState?) => unknown` | yes | The document view at one point, LATEST-shaped, resumable from a stored snapshot's era-pinned document state. The read side's fold: render, references, as-of reads, and the append hooks' state derivation all go through it. |
60
+ | `collectReferences` | `(events) => EventDocLink[]` | yes | The `EventDocLink`s this doc's whole HISTORY depends on; `[]` for a leaf doc type. Invoked by the transfer manifest walk (it exports the whole history). |
61
+ | `collectReferencesFromState` | `(state) => EventDocLink[]` | yes | The `EventDocLink`s the CURRENT state depends on; `[]` for a leaf doc type. Invoked by the references route against a snapshot-seeded folded state. |
62
+ | `render` | `(input: EventDocRenderInput) => EventDocRenderResult \| AskResponse<EventDocRenderResult>` | no | Render the resolved, already-folded document state (`input.state`, resolved snapshot-seeded by the route). Omit and `GET {basePath}/{id}/render` 404s as "no renderer configured". Plain function or story — the dynamic-functions processor runs either. |
63
+
64
+ ### `runtime` `QpqFunctionRuntime` (required)
65
+
66
+ A reference to the module exporting `functions`, usually a relative path string of the form `'/path/to/file::exportedObjectName'` see [defineDynamicFunctions](../core/dynamic-functions.md#runtime--qpqfunctionruntime-required). Must resolve to the SAME object passed as `functions`.
67
+
68
+ ### `options` — `EventDocCollectionOptions` (required)
69
+
70
+ The same object [defineEventDocRoutes](./event-doc-routes.md#parameters) takes, minus `storeName`/`type` (read off `functions` instead). See the [defineEventDocRoutes parameter reference](./event-doc-routes.md#parameters) for every field: `basePath`, `routeAuthSettings`, `version`, `onPublish`, `onAppend`, `scopeResolver`, `excludeRoutes`.
54
71
 
55
72
  ## Examples
56
73
 
57
74
  ```typescript
58
75
  import { defineEventDoc } from 'quidproquo-features';
76
+ import { articleDefinition } from './articleDefinition';
59
77
 
60
78
  export default [
61
79
  // A single "article" collection with authenticated mutations.
62
- ...defineEventDoc({
63
- storeName: 'content',
64
- type: 'article',
80
+ ...defineEventDoc(articleDefinition, '/entry/eventDocs::articleDefinition', {
65
81
  basePath: '/articles',
66
82
  routeAuthSettings: { userDirectoryName: 'editors' },
67
- eventValidator: 'validateArticleEvent',
68
- eventRenderer: 'renderArticle',
69
83
  }),
70
84
  ];
71
85
  ```
@@ -74,6 +88,7 @@ export default [
74
88
 
75
89
  - [defineEventDocSummary](./event-doc-summary.md) — the store half of this helper; call it directly when several types share one store.
76
90
  - [defineEventDocRoutes](./event-doc-routes.md) — the routes half; call it per type in the split setup.
91
+ - [defineDynamicFunctions](../core/dynamic-functions.md) — the registration this threads `functions`/`runtime` through.
77
92
  - [defineKeyValueStore](../core/key-value-store.md) / [defineStorageDrive](../core/storage-drive.md) — the underlying core config the summary helper composes.
78
93
  - **Reading a document in your own stories:** [askEventDocGetByIdOrThrow](../../actions/features/event-doc/ask-event-doc-get-by-id.md), [askEventDocList](../../actions/features/event-doc/ask-event-doc-list.md), and the [version reads](../../actions/features/event-doc/ask-event-doc-get-draft.md).
79
94
  - **Creating a document in your own stories:** [askEventDocCreate](../../actions/features/event-doc/ask-event-doc-create.md).
@@ -11,8 +11,9 @@ The pairing matters: transferring collections declared with [defineTenantedEvent
11
11
 
12
12
  ```typescript
13
13
  import { defineTenant, defineTenantedEventDoc, defineTenantedEventDocTransfer } from 'quidproquo-features';
14
+ import { articleDefinition } from './articleDefinition';
14
15
 
15
- const collections = [{ storeName: 'content', type: 'article' }];
16
+ const collections = [articleDefinition];
16
17
 
17
18
  export default [
18
19
  ...defineTenant({
@@ -21,9 +22,7 @@ export default [
21
22
  routeAuthSettings: { userDirectoryName: 'users' },
22
23
  }),
23
24
 
24
- ...defineTenantedEventDoc({
25
- storeName: 'content',
26
- type: 'article',
25
+ ...defineTenantedEventDoc(articleDefinition, '/entry/eventDocs::articleDefinition', {
27
26
  basePath: '/articles',
28
27
  routeAuthSettings: { userDirectoryName: 'users' },
29
28
  }),
@@ -11,6 +11,7 @@ The deploying service must still register the resolver implementation by calling
11
11
 
12
12
  ```typescript
13
13
  import { defineTenantedEventDoc, defineTenant } from 'quidproquo-features';
14
+ import { articleDefinition } from './articleDefinition';
14
15
 
15
16
  export default [
16
17
  ...defineTenant({
@@ -19,9 +20,7 @@ export default [
19
20
  routeAuthSettings: { userDirectoryName: 'users' },
20
21
  }),
21
22
 
22
- ...defineTenantedEventDoc({
23
- storeName: 'content',
24
- type: 'article',
23
+ ...defineTenantedEventDoc(articleDefinition, '/entry/eventDocs::articleDefinition', {
25
24
  basePath: '/articles',
26
25
  routeAuthSettings: { userDirectoryName: 'users' },
27
26
  }),
@@ -31,14 +30,18 @@ export default [
31
30
  ## Signature
32
31
 
33
32
  ```typescript
34
- function defineTenantedEventDoc(options: TenantedEventDocOptions): QPQConfig;
33
+ function defineTenantedEventDoc(
34
+ functions: EventDocFunctions,
35
+ runtime: QpqFunctionRuntime,
36
+ options: TenantedEventDocCollectionOptions,
37
+ ): QPQConfig;
35
38
  ```
36
39
 
37
- `TenantedEventDocOptions` is `EventDocRoutesOptions` with `scopeResolver` omitted — see [defineEventDoc](./event-doc.md#parameters) for the remaining options.
40
+ `TenantedEventDocCollectionOptions` is `EventDocCollectionOptions` with `scopeResolver` omitted — see [defineEventDoc](./event-doc.md#parameters) for the remaining options.
38
41
 
39
42
  ## Parameters
40
43
 
41
- Same as [defineEventDoc](./event-doc.md#parameters): `storeName`, `type`, `basePath`, `routeAuthSettings`, `version`, `eventValidator`, `eventRenderer`, `onPublish` (without `scopeResolver`, which this always sets to `TENANT_SCOPE_RESOLVER_FN`).
44
+ Same as [defineEventDoc](./event-doc.md#parameters): `functions`, `runtime`, and `options` (`basePath`, `routeAuthSettings`, `version`, `onPublish`, `onAppend`, without `scopeResolver`, which this always sets to `TENANT_SCOPE_RESOLVER_FN`).
42
45
 
43
46
  ## Returns
44
47