graphddb 0.7.9 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/cdc/index.d.ts +389 -4
- package/dist/cdc/index.js +4 -3
- package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
- package/dist/chunk-HNY2EJPV.js +1184 -0
- package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
- package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LGHSZIEE.js +187 -0
- package/dist/chunk-N4NWYNGZ.js +1987 -0
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -252
- package/dist/index.d.ts +23 -1548
- package/dist/index.js +100 -1778
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
- package/dist/linter/index.d.ts +39 -6
- package/dist/linter/index.js +22 -4
- package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
- package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -4
- package/dist/spec/index.js +36 -17
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +4 -3
- package/dist/transform/index.d.ts +460 -1
- package/dist/transform/index.js +2085 -2
- package/dist/types-2PMXEn5x.d.ts +1205 -0
- package/dist/types-BXLzIcQD.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +28 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +113 -66
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +20 -5
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-DanwjE5b.d.ts +0 -327
- package/dist/index-CtPJSMrc.d.ts +0 -934
- package/dist/relation-depth-Dg3yhl7S.d.ts +0 -36
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { d as SegmentedKey, e as ProjectionTransform, f as MaintainEvent, g as ProjectionMap, h as MaintainConsistency, i as MaintainUpdateMode, E as EffectPath, j as MembershipPredicate } from './key-DR7_lpyk.js';
|
|
2
|
+
|
|
3
|
+
type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
|
|
4
|
+
interface FieldOptions {
|
|
5
|
+
format?: 'datetime' | 'date';
|
|
6
|
+
readonly?: boolean;
|
|
7
|
+
serialize?: (value: unknown) => unknown;
|
|
8
|
+
deserialize?: (value: unknown) => unknown;
|
|
9
|
+
/**
|
|
10
|
+
* Optional human-readable description of the field (issue #154). Pure
|
|
11
|
+
* documentation metadata — it does NOT affect storage, hydration, key handling,
|
|
12
|
+
* or any runtime behaviour. When present it is propagated to the field entry in
|
|
13
|
+
* `manifest.json` ({@link ManifestField.description}) and surfaces as a field
|
|
14
|
+
* comment in the generated Python `types.py`. Absent → byte-for-byte unchanged
|
|
15
|
+
* output (backward compatible).
|
|
16
|
+
*/
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
interface FieldMetadata {
|
|
20
|
+
propertyName: string;
|
|
21
|
+
dynamoType: DynamoType;
|
|
22
|
+
options?: FieldOptions;
|
|
23
|
+
/**
|
|
24
|
+
* For a `@literal(...)` field — a `string`-stored field whose value is one of a
|
|
25
|
+
* known, finite set — the allowed literal values, in declaration order (issue
|
|
26
|
+
* #75). Recorded purely as model metadata so a Contract param bound into the
|
|
27
|
+
* field can recover the precise `literal` param-kind from its bind position
|
|
28
|
+
* (`src/spec/contracts.ts`), serializing as `{ type: 'literal', literals: [...] }`
|
|
29
|
+
* instead of the imprecise `'string'`. JSON-safe by construction (a non-empty
|
|
30
|
+
* array of strings). `undefined` for every other field decorator (`@string`,
|
|
31
|
+
* `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
|
|
32
|
+
*/
|
|
33
|
+
literals?: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* For an `@ttl` field (issue #172, Epic #167 — CFn generator C4) — marks this
|
|
36
|
+
* field as the model's **DynamoDB Time-To-Live** attribute. `@ttl` is a
|
|
37
|
+
* standalone semantic decorator pinned to `number` (it records `dynamoType: 'N'`
|
|
38
|
+
* itself, so `@ttl expiresAt!: string` is a compile error), because DynamoDB TTL
|
|
39
|
+
* requires a Number attribute holding **epoch seconds**. It is a **physical
|
|
40
|
+
* schema** fact (unlike the maintenance signals kept off the manifest): the flag
|
|
41
|
+
* flows through {@link EntityMetadata} into `manifest.json`
|
|
42
|
+
* ({@link ManifestEntity.ttlAttribute}), which the CloudFormation emitter consumes
|
|
43
|
+
* to render `TimeToLiveSpecification`. DynamoDB allows exactly one TTL attribute
|
|
44
|
+
* per table, enforced loudly at model registration (`@model`). `undefined` for
|
|
45
|
+
* every other field decorator, so non-TTL fields are byte-for-byte unchanged.
|
|
46
|
+
*/
|
|
47
|
+
ttl?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The maintenance kind of a model (issue #152). `'entity'` (the default) is a plain
|
|
52
|
+
* stored entity. `'materializedView'` / `'sparseView'` mark a model as a maintained
|
|
53
|
+
* **view** row whose `@maintainedFrom` declarations feed the maintenance graph (the
|
|
54
|
+
* adapter that replaces the old `defineView` / `ViewRegistry` path). A `sparseView`
|
|
55
|
+
* requires every `@maintainedFrom` to carry a `when` membership predicate.
|
|
56
|
+
*/
|
|
57
|
+
type ModelKind = 'entity' | 'materializedView' | 'sparseView';
|
|
58
|
+
/**
|
|
59
|
+
* One lowered `@maintainedFrom(source, (self, source) => options)` declaration on a
|
|
60
|
+
* view model (issue #152). It is the decorator-declarative replacement for one
|
|
61
|
+
* `defineView` source slice: the symbolic `(self, source) =>` callback has already
|
|
62
|
+
* been evaluated into the SAME payload-rooted IR (`keyBind` → `$.entity.*` key
|
|
63
|
+
* binding, `project` → {@link ProjectionMap}), so the adapter
|
|
64
|
+
* (`src/relation/maintenance-view-adapter.ts`) reconstructs a byte-identical
|
|
65
|
+
* `ViewSourceSlice` from it.
|
|
66
|
+
*
|
|
67
|
+
* `collection` and `when` are mutually exclusive (a row either holds a maintained
|
|
68
|
+
* list or appears/disappears as a whole — the same rule the old builder enforced).
|
|
69
|
+
*/
|
|
70
|
+
interface MaintainedFromMetadata {
|
|
71
|
+
/** Resolves the source entity whose lifecycle events maintain the view row. */
|
|
72
|
+
sourceFactory: () => new (...args: unknown[]) => unknown;
|
|
73
|
+
/** The lifecycle events that maintain the view row (`created`/`updated`/`removed`). */
|
|
74
|
+
on: readonly MaintainEvent[];
|
|
75
|
+
/** The view-row key binding (view-row key field → payload-rooted source value). */
|
|
76
|
+
keyBind: Readonly<Record<string, EffectPath>>;
|
|
77
|
+
/** The projection of the source payload into the view row (target attr → transform). */
|
|
78
|
+
project: ProjectionMap;
|
|
79
|
+
/** Bounded/ordered collection options when this declaration maintains a list. */
|
|
80
|
+
collection?: {
|
|
81
|
+
/** The view-row attribute that holds the maintained list (a `self.<field>`). */
|
|
82
|
+
field: string;
|
|
83
|
+
maxItems?: number;
|
|
84
|
+
order?: 'ASC' | 'DESC';
|
|
85
|
+
/** The source value the collection orders by / trims on (`$.entity.<field>`). */
|
|
86
|
+
orderBy?: EffectPath;
|
|
87
|
+
};
|
|
88
|
+
/** Sparse-view membership predicate (mutually exclusive with `collection`). */
|
|
89
|
+
when?: MembershipPredicate;
|
|
90
|
+
consistency?: MaintainConsistency;
|
|
91
|
+
updateMode?: MaintainUpdateMode;
|
|
92
|
+
/**
|
|
93
|
+
* Optional human-readable description of this maintained-from source slice (issue
|
|
94
|
+
* #166, follow-up of #154), supplied via
|
|
95
|
+
* `@maintainedFrom(() => Source, (self, source) => ({ description, ... }))`. Pure
|
|
96
|
+
* documentation metadata — it does NOT affect the maintenance IR (`keyBind` /
|
|
97
|
+
* `project` / `on` / `collection` / `when`) or any runtime behaviour.
|
|
98
|
+
*
|
|
99
|
+
* NOTE (representation): the maintenance IR is deliberately kept OFF the
|
|
100
|
+
* serializable `manifest.json` / `operations.json` — the {@link Manifest} carries
|
|
101
|
+
* only the physical shape (table / keys / GSIs / relation navigation), NOT the
|
|
102
|
+
* maintenance graph (see `detectStreamMaintenance` in `src/codegen/cloudformation.ts`).
|
|
103
|
+
* There is therefore no manifest / operations / generated-Python site for a
|
|
104
|
+
* maintained-from source; this description is captured at the metadata layer only
|
|
105
|
+
* (mirroring where #154 captured its descriptions before propagation), available to
|
|
106
|
+
* any future consumer that DOES surface `@maintainedFrom` (e.g. the CDC-projection
|
|
107
|
+
* typed-consumer path, #153). Absent → byte-for-byte unchanged metadata.
|
|
108
|
+
*/
|
|
109
|
+
description?: string;
|
|
110
|
+
}
|
|
111
|
+
interface KeyDefinition {
|
|
112
|
+
/** The canonical structured key (PK/SK segment lists). */
|
|
113
|
+
segmented: SegmentedKey;
|
|
114
|
+
inputFieldNames: string[];
|
|
115
|
+
}
|
|
116
|
+
interface GsiDefinition {
|
|
117
|
+
indexName: string;
|
|
118
|
+
/** The canonical structured key (PK/SK segment lists). */
|
|
119
|
+
segmented: SegmentedKey;
|
|
120
|
+
inputFieldNames: string[];
|
|
121
|
+
unique: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Optional human-readable description of the GSI (issue #166, follow-up of #154),
|
|
124
|
+
* supplied via `gsi(name, key, { description })`. Pure documentation metadata — it
|
|
125
|
+
* does NOT affect the index key, projection, or any runtime behaviour. When present
|
|
126
|
+
* it is propagated to the index entry in `manifest.json`
|
|
127
|
+
* ({@link ManifestGsi.description}) and surfaces as the docstring of any generated
|
|
128
|
+
* Python query method that reads through this index (a query carrying the GSI's
|
|
129
|
+
* `indexName`, when the query itself declares no description). Absent →
|
|
130
|
+
* byte-for-byte unchanged output (backward compatible).
|
|
131
|
+
*/
|
|
132
|
+
description?: string;
|
|
133
|
+
}
|
|
134
|
+
interface RelationLimitOptions {
|
|
135
|
+
default: number;
|
|
136
|
+
max: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The named maintenance pattern a relation participates in (Epic #118 §5.1).
|
|
140
|
+
*
|
|
141
|
+
* `pattern` is the *preset* selector: omitting it leaves the relation a plain
|
|
142
|
+
* read-only navigation (the historical `hasMany`/`belongsTo`/`hasOne`
|
|
143
|
+
* behaviour), exactly backward compatible. When present it names the AWS
|
|
144
|
+
* design pattern the relation lowers to — the lowering itself (maintenance
|
|
145
|
+
* graph / compile injection) is out of scope for issue #121 and lands in
|
|
146
|
+
* #124/#125; here the value is recorded purely as declaration metadata.
|
|
147
|
+
*
|
|
148
|
+
* Typed as a `string` union of the known presets but kept open-ended via the
|
|
149
|
+
* `(string & {})` tail so a future preset can be authored before this union is
|
|
150
|
+
* widened, without a breaking change to callers.
|
|
151
|
+
*
|
|
152
|
+
* The union lists only the presets that are actually *reachable* as a recorded
|
|
153
|
+
* `RelationOptions.pattern` / `AggregateOptions.pattern` value: `samePartition`,
|
|
154
|
+
* `counter`, `embeddedSnapshot`, and the two versioned discriminators
|
|
155
|
+
* `versionedLatest` / `versionedHistory` (issue #152). `materializedView` /
|
|
156
|
+
* `sparseView` migrated to the model-level `@model({ kind })` selector (they are
|
|
157
|
+
* a {@link ModelKind}, not a relation preset); the placeholder `edge` /
|
|
158
|
+
* `dualEdge` / `versioned` / `externalProjection` names are not authored anywhere
|
|
159
|
+
* and were removed. A future preset can still be written against the `(string & {})`
|
|
160
|
+
* tail before it is added to this union.
|
|
161
|
+
*/
|
|
162
|
+
type RelationPattern = 'samePartition' | 'counter' | 'embeddedSnapshot' | 'versionedLatest' | 'versionedHistory' | (string & {});
|
|
163
|
+
/**
|
|
164
|
+
* Consistency boundary a maintained write is declared against (Epic #118 §4.A.7):
|
|
165
|
+
* `transactional` = same `TransactWriteItems`; `eventual` = asynchronous catch-up.
|
|
166
|
+
*/
|
|
167
|
+
type RelationConsistency = 'transactional' | 'eventual';
|
|
168
|
+
/**
|
|
169
|
+
* How a maintained write is applied (Epic #118 §4.A.7):
|
|
170
|
+
* `mutation` = composed into the same mutation; `stream` = driven asynchronously
|
|
171
|
+
* off a stream / outbox.
|
|
172
|
+
*/
|
|
173
|
+
type RelationUpdateMode = 'mutation' | 'stream';
|
|
174
|
+
/**
|
|
175
|
+
* Read-strategy hints for a maintained relation (Epic #118 §5.1 `read`).
|
|
176
|
+
*
|
|
177
|
+
* All fields optional — present only when a `pattern` declares a read shape
|
|
178
|
+
* (e.g. an `embeddedSnapshot` keeping the latest N items). A bare relation
|
|
179
|
+
* carries no `read` block.
|
|
180
|
+
*/
|
|
181
|
+
interface RelationReadOptions {
|
|
182
|
+
/** Cap on materialised items kept inline (e.g. "latest 3 posts"). */
|
|
183
|
+
maxItems?: number;
|
|
184
|
+
/** Ordering of the maintained collection. */
|
|
185
|
+
order?: 'ASC' | 'DESC';
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Write-maintenance declaration for a relation (Epic #118 §5.1 `write`).
|
|
189
|
+
*
|
|
190
|
+
* `maintainedOn` is the cross-entity trigger list (`"<Entity>.<event>"`, e.g.
|
|
191
|
+
* `"Post.created"`) that the maintenance graph (#124/#125) will key off. Recorded
|
|
192
|
+
* here as declaration metadata only.
|
|
193
|
+
*/
|
|
194
|
+
interface RelationWriteOptions {
|
|
195
|
+
/**
|
|
196
|
+
* Cross-entity triggers, e.g. `['Post.created', 'Post.removed']`. The event
|
|
197
|
+
* segment is the confirmed shared lifecycle vocabulary (epic #118 A/B
|
|
198
|
+
* reconciliation): `created` / `updated` / `removed`.
|
|
199
|
+
*/
|
|
200
|
+
maintainedOn?: string[];
|
|
201
|
+
consistency?: RelationConsistency;
|
|
202
|
+
updateMode?: RelationUpdateMode;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Snapshot / projection capture map for a maintained relation
|
|
206
|
+
* (Epic #118 §5.1 / §5.2 `projection`).
|
|
207
|
+
*
|
|
208
|
+
* Maps each captured field name on the maintaining side to **how** the source
|
|
209
|
+
* value is projected. The confirmed shared vocabulary (epic #118 A/B
|
|
210
|
+
* reconciliation) is the **function-form IR** — the same {@link
|
|
211
|
+
* ProjectionTransform} node A (#120) defines on `src/define/entity-writes.ts`,
|
|
212
|
+
* re-used here (NOT re-defined) so the two surfaces never re-diverge. Each value
|
|
213
|
+
* is either:
|
|
214
|
+
*
|
|
215
|
+
* - a {@link ProjectionTransform} (built with the standalone authoring helpers,
|
|
216
|
+
* e.g. `preview('body', 120)` — RFC §5.2 `textPreview: preview('body', 120)`),
|
|
217
|
+
* or
|
|
218
|
+
* - a bare `string` — the **identity shorthand**: project that source path
|
|
219
|
+
* through unchanged (e.g. `{ postId: 'postId' }`).
|
|
220
|
+
*
|
|
221
|
+
* `Readonly` so the recorded declaration metadata is not mutated downstream.
|
|
222
|
+
* Issue #121 scopes this to the declaration shape; the maintenance graph /
|
|
223
|
+
* compile injection that consumes the IR is #124/#125.
|
|
224
|
+
*
|
|
225
|
+
* @example `{ postId: 'postId', textPreview: preview('body', 120) }`
|
|
226
|
+
*/
|
|
227
|
+
type RelationProjection = Readonly<Record<string, ProjectionTransform | string>>;
|
|
228
|
+
interface RelationOptions {
|
|
229
|
+
limit?: RelationLimitOptions;
|
|
230
|
+
order?: 'ASC' | 'DESC';
|
|
231
|
+
/**
|
|
232
|
+
* Optional human-readable description of the relation (issue #166, follow-up of
|
|
233
|
+
* #154), supplied via `@hasMany/@belongsTo/@hasOne(() => T, keyBind, { description })`.
|
|
234
|
+
* Pure documentation metadata — it does NOT affect navigation, key binding, or any
|
|
235
|
+
* runtime behaviour. When present it is propagated to the relation entry in
|
|
236
|
+
* `manifest.json` ({@link ManifestRelation.description}) and surfaces as a trailing
|
|
237
|
+
* `# …` comment on the relation's field in the generated Python result TypedDict /
|
|
238
|
+
* dataclass. Absent → byte-for-byte unchanged output (backward compatible).
|
|
239
|
+
*/
|
|
240
|
+
description?: string;
|
|
241
|
+
/**
|
|
242
|
+
* Named maintenance preset (Epic #118). Omitted = plain read-only navigation,
|
|
243
|
+
* fully backward compatible with the historical relation decorators.
|
|
244
|
+
*/
|
|
245
|
+
pattern?: RelationPattern;
|
|
246
|
+
read?: RelationReadOptions;
|
|
247
|
+
write?: RelationWriteOptions;
|
|
248
|
+
projection?: RelationProjection;
|
|
249
|
+
/**
|
|
250
|
+
* Lowered versioned-pattern declaration (issue #152, section B). Present only when
|
|
251
|
+
* the relation declares `pattern: 'versionedLatest' | 'versionedHistory'` via the
|
|
252
|
+
* `@hasOne/@hasMany(() => Target, (self, source) => ({...}))` callback form. The
|
|
253
|
+
* declaring model is the SOURCE (a revision); the relation target is the
|
|
254
|
+
* DESTINATION view row. The adapter
|
|
255
|
+
* (`src/relation/maintenance-view-adapter.ts`) lowers this to the SAME `defineView`
|
|
256
|
+
* snapshot IR the old `defineVersioned` produced (latest = overwrite, history =
|
|
257
|
+
* append). `on` are the lifecycle events; `project` is the lowered projection map.
|
|
258
|
+
*/
|
|
259
|
+
versioned?: {
|
|
260
|
+
readonly mode: 'latest' | 'history';
|
|
261
|
+
readonly on: readonly MaintainEvent[];
|
|
262
|
+
readonly project: ProjectionMap;
|
|
263
|
+
readonly consistency?: MaintainConsistency;
|
|
264
|
+
readonly updateMode?: MaintainUpdateMode;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Descriptor for a `refs` relation (issue #197, Pattern 2 Embedded Refs read
|
|
269
|
+
* side). A `refs` relation resolves an **inline id-list** held on the parent row
|
|
270
|
+
* — e.g. `PPost.tagRefs: { tagId: string }[]` — into the referenced child
|
|
271
|
+
* bodies, in ONE deduped `BatchGetItem` (never per-ref `GetItem`).
|
|
272
|
+
*
|
|
273
|
+
* Unlike `hasMany`/`belongsTo`/`hasOne`, whose key is bound from a parent
|
|
274
|
+
* **scalar** field ({@link RelationMetadata.keyBinding} — `target key ← parent
|
|
275
|
+
* scalar`), a `refs` key is bound from EACH ELEMENT of a parent LIST attribute:
|
|
276
|
+
* the traversal reads `parent[from]` (a list), pulls `.key` off every element,
|
|
277
|
+
* and fans those out into a single BatchGet against the target. This descriptor
|
|
278
|
+
* captures exactly that list-valued source shape, which the scalar `keyBinding`
|
|
279
|
+
* `Record<string,string>` cannot express.
|
|
280
|
+
*
|
|
281
|
+
* `keyBinding` on a `refs` relation still records the **target child key field ←
|
|
282
|
+
* element key name** mapping (e.g. `{ tagId: 'tagId' }`), so key building /
|
|
283
|
+
* projection reuse the existing scalar machinery per resolved element; `refs`
|
|
284
|
+
* only adds *where the scalar values come from* (the parent list), not a new key
|
|
285
|
+
* grammar. That keeps `refs` byte-compatible with every scalar-keyBinding
|
|
286
|
+
* consumer (manifest, planner, dedupe) — an unknown-to-them relation kind is
|
|
287
|
+
* simply carried through with a valid scalar `keyBinding`.
|
|
288
|
+
*/
|
|
289
|
+
interface RefsBinding {
|
|
290
|
+
/** The parent LIST attribute holding the inline reference elements (e.g. `'tagRefs'`). */
|
|
291
|
+
from: string;
|
|
292
|
+
/** The key field to read off EACH list element (e.g. `'tagId'`). */
|
|
293
|
+
key: string;
|
|
294
|
+
}
|
|
295
|
+
interface RelationMetadata {
|
|
296
|
+
type: 'hasMany' | 'hasOne' | 'belongsTo' | 'refs';
|
|
297
|
+
propertyName: string;
|
|
298
|
+
targetFactory: () => new (...args: unknown[]) => unknown;
|
|
299
|
+
keyBinding: Record<string, string>;
|
|
300
|
+
/**
|
|
301
|
+
* List-valued source descriptor for a `refs` relation (issue #197). Present
|
|
302
|
+
* IFF `type === 'refs'`; the traversal reads the parent list `refs.from`, pulls
|
|
303
|
+
* `refs.key` off each element, and resolves the referenced child bodies in one
|
|
304
|
+
* deduped BatchGet. Absent for every scalar-keyed relation (byte-compatible).
|
|
305
|
+
*/
|
|
306
|
+
refs?: RefsBinding;
|
|
307
|
+
options?: RelationOptions;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The scalar-aggregate value an `@aggregate` field derives from its source entity
|
|
311
|
+
* (Epic #118 §5.2 counter / latest preset; issue #122). It is the function-form
|
|
312
|
+
* IR built by the {@link count} / {@link max} value helpers — the aggregation an
|
|
313
|
+
* `@aggregate(() => Source, keyBinding, { pattern, value })` field maintains:
|
|
314
|
+
*
|
|
315
|
+
* - `count` — the cardinality of source rows matched by the key binding (no
|
|
316
|
+
* source field; e.g. `postCount!: number` ← `count()`).
|
|
317
|
+
* - `max` — the maximum value of a named source `field` (e.g. `lastPostAt!:
|
|
318
|
+
* string` ← `max('createdAt')`).
|
|
319
|
+
*
|
|
320
|
+
* Recorded purely as declaration metadata (issue #122 scope = declaration
|
|
321
|
+
* surface): the maintenance graph (#124) / compile injection (#125) consume it;
|
|
322
|
+
* the effect itself is out of scope here. Kept open-ended via the discriminated
|
|
323
|
+
* `op` so a future aggregation (`min`, `sum`, …) extends the union without a
|
|
324
|
+
* breaking change.
|
|
325
|
+
*
|
|
326
|
+
* @example `count()` → `{ op: 'count' }`
|
|
327
|
+
* @example `max('createdAt')` → `{ op: 'max', field: 'createdAt' }`
|
|
328
|
+
*/
|
|
329
|
+
type AggregateValue = {
|
|
330
|
+
readonly op: 'count';
|
|
331
|
+
} | {
|
|
332
|
+
readonly op: 'max';
|
|
333
|
+
readonly field: string;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* Options for an `@aggregate` field (Epic #118 §5.2 counter / latest preset;
|
|
337
|
+
* issue #122). Mirrors the maintenance vocabulary a relation declares so the two
|
|
338
|
+
* authoring surfaces stay in lockstep:
|
|
339
|
+
*
|
|
340
|
+
* - `pattern` reuses {@link RelationPattern} (e.g. `'counter'`) — the named
|
|
341
|
+
* maintenance preset, NOT re-defined here.
|
|
342
|
+
* - `value` is the {@link AggregateValue} the field maintains (`count()` /
|
|
343
|
+
* `max('createdAt')`).
|
|
344
|
+
* - `write` reuses {@link RelationWriteOptions} verbatim — `maintainedOn`
|
|
345
|
+
* triggers (`Post.created` / `Post.removed`, the confirmed shared
|
|
346
|
+
* `created|updated|removed` event vocabulary) plus `consistency`
|
|
347
|
+
* (`transactional|eventual`) / `updateMode` (`mutation|stream`). These are NOT
|
|
348
|
+
* re-defined; they are the same shared types B (#121) added.
|
|
349
|
+
*/
|
|
350
|
+
interface AggregateOptions {
|
|
351
|
+
/** Named maintenance preset (typically `'counter'`); reuses {@link RelationPattern}. */
|
|
352
|
+
pattern?: RelationPattern;
|
|
353
|
+
/** The scalar aggregation this field maintains (`count()` / `max('createdAt')`). */
|
|
354
|
+
value: AggregateValue;
|
|
355
|
+
/** Cross-entity maintenance triggers / consistency / update-mode (reused from B). */
|
|
356
|
+
write?: RelationWriteOptions;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Declaration metadata recorded by an `@aggregate` field decorator (issue #122).
|
|
360
|
+
*
|
|
361
|
+
* An `@aggregate` is a **scalar** field (`postCount!: number`,
|
|
362
|
+
* `lastPostAt!: string`) whose value is derived — by aggregating rows of a source
|
|
363
|
+
* entity resolved by {@link targetFactory} + {@link keyBinding} — rather than
|
|
364
|
+
* stored directly. It is therefore recorded in its **own** metadata array
|
|
365
|
+
* (`EntityMetadata.aggregates`), distinct from both {@link FieldMetadata} (a
|
|
366
|
+
* plain stored attribute) and {@link RelationMetadata} (a navigation yielding
|
|
367
|
+
* model instances): an aggregate carries a source binding like a relation, but
|
|
368
|
+
* surfaces a scalar value like a field. The collector path mirrors relations
|
|
369
|
+
* (decorator pushes, `@model` drains).
|
|
370
|
+
*
|
|
371
|
+
* Issue #122 scope is the declaration surface + metadata pass-through only; the
|
|
372
|
+
* aggregation effect's compile / runtime (#124/#125/#127) is out of scope.
|
|
373
|
+
*/
|
|
374
|
+
interface AggregateMetadata {
|
|
375
|
+
/** The scalar field the aggregate value is written to. */
|
|
376
|
+
propertyName: string;
|
|
377
|
+
/** Resolves the source entity whose rows are aggregated. */
|
|
378
|
+
targetFactory: () => new (...args: unknown[]) => unknown;
|
|
379
|
+
/** The key binding selecting the source rows (target key field → source field). */
|
|
380
|
+
keyBinding: Record<string, string>;
|
|
381
|
+
/** The aggregate preset / value / maintenance-write declaration. */
|
|
382
|
+
options: AggregateOptions;
|
|
383
|
+
}
|
|
384
|
+
interface EmbeddedMetadata {
|
|
385
|
+
propertyName: string;
|
|
386
|
+
modelFactory: () => new (...args: unknown[]) => unknown;
|
|
387
|
+
}
|
|
388
|
+
interface EntityMetadata {
|
|
389
|
+
tableName: string;
|
|
390
|
+
prefix: string;
|
|
391
|
+
fields: FieldMetadata[];
|
|
392
|
+
primaryKey: KeyDefinition | null;
|
|
393
|
+
gsiDefinitions: GsiDefinition[];
|
|
394
|
+
relations: RelationMetadata[];
|
|
395
|
+
/** Scalar `@aggregate` fields (Epic #118 §5.2 counter / latest; issue #122). */
|
|
396
|
+
aggregates: AggregateMetadata[];
|
|
397
|
+
embeddedFields: EmbeddedMetadata[];
|
|
398
|
+
/**
|
|
399
|
+
* The model's maintenance kind (issue #152). `'entity'` (default; also assumed
|
|
400
|
+
* when absent, for hand-built metadata in tests) is a plain stored entity;
|
|
401
|
+
* `'materializedView'` / `'sparseView'` mark it a maintained view row fed by
|
|
402
|
+
* {@link maintainedFrom}.
|
|
403
|
+
*/
|
|
404
|
+
kind?: ModelKind;
|
|
405
|
+
/**
|
|
406
|
+
* Optional human-readable description of the entity (issue #154), supplied via
|
|
407
|
+
* `@model({ description })`. Pure documentation metadata — it does NOT affect
|
|
408
|
+
* storage, keys, or any runtime behaviour. When present it is propagated to the
|
|
409
|
+
* entity entry in `manifest.json` ({@link ManifestEntity.description}) and
|
|
410
|
+
* surfaces as the class docstring in the generated Python `types.py`. Absent →
|
|
411
|
+
* byte-for-byte unchanged output (backward compatible).
|
|
412
|
+
*/
|
|
413
|
+
description?: string;
|
|
414
|
+
/**
|
|
415
|
+
* Class-level `@maintainedFrom(...)` declarations (issue #152). Non-empty only on
|
|
416
|
+
* a `materializedView` / `sparseView` model; each is one source slice the view
|
|
417
|
+
* row is maintained from. The adapter lowers these to the maintenance graph's
|
|
418
|
+
* `ViewDefinition` IR (replacing the old `defineView` / `ViewRegistry` path).
|
|
419
|
+
* Absent / empty for a plain entity.
|
|
420
|
+
*/
|
|
421
|
+
maintainedFrom?: MaintainedFromMetadata[];
|
|
422
|
+
/**
|
|
423
|
+
* Whether the model is flagged **CDC-parseable** by `@cdcProjected()` (issue
|
|
424
|
+
* #153). It carries NO runtime side effect — it is purely a compile-/registration-
|
|
425
|
+
* time capability marker: `Model.fromChange(event)` (and, transitively,
|
|
426
|
+
* `DDBModel.subscribe` routing to this model) is gated on it, and it is the SSoT
|
|
427
|
+
* the codegen reads to emit the `CdcModelRegistry` module augmentation that types
|
|
428
|
+
* `subscribe`'s handlers by model name. `true` only on a model carrying the
|
|
429
|
+
* decorator; absent (⇒ falsy) for every plain entity, so a non-`@cdcProjected`
|
|
430
|
+
* model is byte-for-byte unchanged (backward compatible). Deliberately kept OFF
|
|
431
|
+
* the manifest / bridge (it is a host-runtime + TS-typing concern, like the
|
|
432
|
+
* maintenance signals — not a physical-schema fact like {@link ttlAttribute}).
|
|
433
|
+
*/
|
|
434
|
+
cdcProjected?: boolean;
|
|
435
|
+
/**
|
|
436
|
+
* The physical attribute name of the model's `@ttl` field, when one is declared
|
|
437
|
+
* (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
|
|
438
|
+
* Number attribute holding epoch seconds; `@model` derives this from the (unique)
|
|
439
|
+
* field carrying {@link FieldMetadata.ttl} and throws if more than one field on the
|
|
440
|
+
* SAME model is `@ttl` (the cross-entity, same-physical-table single-TTL rule is
|
|
441
|
+
* enforced downstream, at manifest build, where all entities on a table are known).
|
|
442
|
+
* A non-key field's physical attribute name is its property name, so this is the
|
|
443
|
+
* `@ttl` field's property name. Propagated to the manifest
|
|
444
|
+
* ({@link ManifestEntity.ttlAttribute}) — the SSoT the CloudFormation emitter reads
|
|
445
|
+
* to render `TimeToLiveSpecification`. Absent when the model declares no `@ttl`.
|
|
446
|
+
*/
|
|
447
|
+
ttlAttribute?: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export type { AggregateOptions as A, DynamoType as D, EntityMetadata as E, FieldOptions as F, ModelKind as M, RelationOptions as R, AggregateValue as a };
|
package/docs/cdc-projection.md
CHANGED
|
@@ -80,7 +80,7 @@ A valid event for this model always has at least one non-null side, so `[null, n
|
|
|
80
80
|
is an unambiguous "this event is not for this model" signal. You don't need a separate
|
|
81
81
|
guard — `fromChange` does routing **and** parse in one call.
|
|
82
82
|
|
|
83
|
-
### 3. `
|
|
83
|
+
### 3. `graphddb.subscribe(handlers)` — declarative, batch
|
|
84
84
|
|
|
85
85
|
The third sibling of `query` / `mutate` (GraphQL's query / mutation / **subscription**).
|
|
86
86
|
It takes a handler map keyed by model name. Unlike `query` / `mutate`, which hit
|
|
@@ -89,7 +89,7 @@ DynamoDB, **`subscribe` does not subscribe**: it *returns* a `ChangeHandler`
|
|
|
89
89
|
|
|
90
90
|
```ts
|
|
91
91
|
// graphddb side: build a batch handler that parses + routes + dispatches (no sink, no dedup)
|
|
92
|
-
const handler =
|
|
92
|
+
const handler = graphddb.subscribe({
|
|
93
93
|
OrderModel: (old, neu) => { // old, neu: OrderModel | null (typed via the generated registry)
|
|
94
94
|
if (!neu) return opensearch.delete({ id: old!.orderId });
|
|
95
95
|
return opensearch.index({ id: neu.orderId, body: { status: neu.status } });
|
|
@@ -138,9 +138,9 @@ inferred from the key.
|
|
|
138
138
|
|
|
139
139
|
| GraphQL | DDBModel | Executed by | Returns |
|
|
140
140
|
| --- | --- | --- | --- |
|
|
141
|
-
| query | `
|
|
142
|
-
| mutation | `
|
|
143
|
-
| subscription | `
|
|
141
|
+
| query | `graphddb.query(envelope)` | graphddb (hits DynamoDB) | result object |
|
|
142
|
+
| mutation | `graphddb.mutate(envelope)` | graphddb (hits DynamoDB) | result object |
|
|
143
|
+
| subscription | `graphddb.subscribe(handlers)` | **you** (mount it) | `ChangeHandler` |
|
|
144
144
|
|
|
145
145
|
Only subscription is "executed by you, returns a handler rather than a result" —
|
|
146
146
|
because CDC delivery lives outside the boundary.
|
package/docs/class-hydration.md
CHANGED
|
@@ -267,7 +267,7 @@ serialized query/`OperationSpec`, or `operations.json`. This is the same rule
|
|
|
267
267
|
`#50` (middleware) and `updatable` / `consistentRead` already follow.
|
|
268
268
|
|
|
269
269
|
- **The declarative layer is "what to fetch."** `operations.json` is generated
|
|
270
|
-
from *contract definitions* (`
|
|
270
|
+
from *contract definitions* (`publishQuery`, etc.) — key, select, filter,
|
|
271
271
|
projection, result paths, cardinality, execution plan. It is the serializable,
|
|
272
272
|
language-neutral plan the Python runtime (`#48`) replays. It already contains
|
|
273
273
|
**none** of the host-runtime call options: a `grep` of
|
package/docs/cqrs-contract.md
CHANGED
|
@@ -83,7 +83,9 @@ field list argument. The access pattern — primary key or GSI — is resolved f
|
|
|
83
83
|
the `Model` plus the `key` fields named in the descriptor:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
|
|
86
|
+
import { graphddb, param } from 'graphddb';
|
|
87
|
+
|
|
88
|
+
const UserByEmail = graphddb.publishQuery({
|
|
87
89
|
get: { query: User, key: { email: param.string() }, select: { userId: true, email: true } },
|
|
88
90
|
});
|
|
89
91
|
```
|
|
@@ -95,7 +97,7 @@ key, select, options? }`, with exactly one of `query` / `list`. `select` and
|
|
|
95
97
|
`options` are separate keys (no `select` nested inside `options`).
|
|
96
98
|
|
|
97
99
|
```ts
|
|
98
|
-
export const ArticleById =
|
|
100
|
+
export const ArticleById = graphddb.publishQuery({
|
|
99
101
|
get: { query: Article, key: { articleId: param.string() },
|
|
100
102
|
select: { articleId: true, title: true, body: true },
|
|
101
103
|
options: { consistentRead: false } },
|
|
@@ -193,7 +195,7 @@ write-use-case Methods. A command method is a single declarative write
|
|
|
193
195
|
result?, mode? }`, with exactly one of `create` / `update` / `remove`.
|
|
194
196
|
|
|
195
197
|
```ts
|
|
196
|
-
export const UserCommands =
|
|
198
|
+
export const UserCommands = graphddb.publishCommand({
|
|
197
199
|
disable: { update: User, key: { userId: param.string() },
|
|
198
200
|
input: { status: 'disabled', disableReason: param.string() },
|
|
199
201
|
result: { select: { userId: true, status: true } } },
|
|
@@ -214,7 +216,7 @@ export const UserCommands = publicCommandModel({
|
|
|
214
216
|
**or** a raw `cond\`…\`` fragment whose value slots may be `param.*` bindings:
|
|
215
217
|
|
|
216
218
|
```ts
|
|
217
|
-
export const UserCommands =
|
|
219
|
+
export const UserCommands = graphddb.publishCommand({
|
|
218
220
|
// declarative operators in a write condition
|
|
219
221
|
promote: { update: User, key: { userId: param.string() },
|
|
220
222
|
input: { role: 'admin' },
|
|
@@ -276,7 +278,7 @@ read dependency, declared with the `query($ => ({...}))` builder; a cross-
|
|
|
276
278
|
fragment reference uses `$.alias.field`:
|
|
277
279
|
|
|
278
280
|
```ts
|
|
279
|
-
export const AccountAccess =
|
|
281
|
+
export const AccountAccess = graphddb.publishQuery({
|
|
280
282
|
get: query($ => ({
|
|
281
283
|
account: { query: Account, key: { accountId: param.string() },
|
|
282
284
|
select: { accountId: true } },
|
|
@@ -361,7 +363,8 @@ single-model lint framework.
|
|
|
361
363
|
|
|
362
364
|
A method body is evaluated at definition time with throwing sentinels in place
|
|
363
365
|
of `keys` / `params`, recorded through a model recorder, and subjected to the
|
|
364
|
-
same hardening
|
|
366
|
+
same no-runtime-capture build-time hardening the prepared-statement compiler
|
|
367
|
+
applies:
|
|
365
368
|
|
|
366
369
|
1. **Throwing sentinel Proxy** — any property / method access other than the
|
|
367
370
|
internal brand reads or primitive coercion throws at the access site.
|
|
@@ -489,31 +492,36 @@ input produces a byte-identical pre-contract document).
|
|
|
489
492
|
|
|
490
493
|
## Public API surface
|
|
491
494
|
|
|
492
|
-
Definition DSL (`src/define/contract.ts`)
|
|
495
|
+
Definition DSL (`src/define/contract.ts`) — import the `graphddb` namespace
|
|
496
|
+
(`import { graphddb } from 'graphddb'`); `graphddb.publishQuery` /
|
|
497
|
+
`graphddb.publishCommand` are the sole read/write authoring surface:
|
|
493
498
|
|
|
494
499
|
```ts
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
query($ => ({ alias: descriptor, ... })) // composite read; cross-fragment refs $.alias.field
|
|
498
|
-
mutation($ => ({ alias: descriptor, ... })) // composite write; cross-fragment refs $.alias.field
|
|
500
|
+
graphddb.publishQuery({ alias: { query | list: Model, key, select, options? } })
|
|
501
|
+
graphddb.publishCommand({ alias: { create | update | remove: Model, key, input, condition?, result?, use?, mode? } })
|
|
499
502
|
param.string() / param.number() / ... // descriptor key/input parameters
|
|
500
503
|
```
|
|
501
504
|
|
|
502
|
-
|
|
505
|
+
A query method may compose another query contract's method (cross-fragment refs
|
|
506
|
+
via `$.alias.field`); a command descriptor composes its derived write ops and
|
|
507
|
+
maintenance writes into one plan. These are declared inside the
|
|
508
|
+
`publishQuery` / `publishCommand` map — there is no separate public `query()` /
|
|
509
|
+
`mutation()` composite verb (removed in 0.8.0).
|
|
503
510
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
executeCommandMethod(contract, methodName, key | keys[], params?) // command-runtime
|
|
507
|
-
encodePerKeyCursor / decodePerKeyCursor / serializeContractKey // per-key-cursor
|
|
508
|
-
```
|
|
511
|
+
Execution is the two `graphddb.*` verbs (`graphddb.query` / `graphddb.mutate`) —
|
|
512
|
+
they route contract methods through the in-process runtime.
|
|
509
513
|
|
|
510
|
-
|
|
514
|
+
The contract runtime (`executeQueryMethod` / `executeCommandMethod` /
|
|
515
|
+
per-key-cursor helpers, `src/runtime/*`) is **internal** machinery behind those
|
|
516
|
+
verbs — not a public export.
|
|
517
|
+
|
|
518
|
+
Build / static analysis is the advanced `graphddb/spec` subpath:
|
|
511
519
|
|
|
512
520
|
```ts
|
|
521
|
+
import { buildContracts, buildContexts } from 'graphddb/spec';
|
|
522
|
+
|
|
513
523
|
buildContracts(contracts) // → { contracts, queries, commands, transactions }
|
|
514
524
|
buildContexts(contexts) // → context-ownership specs
|
|
515
|
-
assertContractN1Safe(name, spec, isGsiPoint?)
|
|
516
|
-
assertContractBoundaries(...) // context-boundary lint
|
|
517
525
|
```
|
|
518
526
|
|
|
519
527
|
## Query vs Command
|
package/docs/design-patterns.md
CHANGED
|
@@ -542,11 +542,11 @@ GSI). *Dual-edge form* — declare `w.dualEdge(forward, inverse)` on the forward
|
|
|
542
542
|
keep a **two-row** bidirectional edge in sync without an inverse GSI. The dual edge can
|
|
543
543
|
be recorded in either of two write vocabularies carrying the SAME `w.dualEdge`:
|
|
544
544
|
|
|
545
|
-
- **`entityWrites` (recommended when driving through the command
|
|
545
|
+
- **`entityWrites` (recommended when driving through the command IF, #195)** —
|
|
546
546
|
place `w.dualEdge(...)` in a lifecycle's `edges` array. The dual edge is then derived by
|
|
547
|
-
`
|
|
548
|
-
rows in the SAME atomic transaction with no low-level handwork. Use
|
|
549
|
-
is created / removed through a command.
|
|
547
|
+
the command runtime behind `graphddb.publishCommand` / `graphddb.mutate`, so a write over
|
|
548
|
+
the edge maintains BOTH rows in the SAME atomic transaction with no low-level handwork. Use
|
|
549
|
+
this form if the edge is created / removed through a command.
|
|
550
550
|
- **`edgeWrites` (the low-level edge-only primitive)** — the historical adjacency-only
|
|
551
551
|
`writes` member. Same two-row synchronization, but driven through the low-level edge-write
|
|
552
552
|
path rather than the command IF.
|
|
@@ -626,7 +626,7 @@ actual sink write happens inside the consumer's CDC infrastructure, outside grap
|
|
|
626
626
|
That runtime was **removed in 0.4.0** (issue #152). External projection is now a
|
|
627
627
|
**typed-consumer-IF contract** (issue #153): mark the source model with
|
|
628
628
|
`@cdcProjected()`, then parse a CDC change event into typed instances — `Model.fromChange(event)`
|
|
629
|
-
returns `[oldRecord, newRecord]`, and `
|
|
629
|
+
returns `[oldRecord, newRecord]`, and `graphddb.subscribe(handlers)` (the GraphQL-style
|
|
630
630
|
sibling of `query` / `mutate`) returns a `ChangeHandler` the consumer mounts on its own
|
|
631
631
|
stream. graphddb owns the *parse → typed record* contract; subscription, sink delivery,
|
|
632
632
|
and idempotency stay with the consumer. See [`cdc-projection.md`](./cdc-projection.md)
|
package/docs/docs-generation.md
CHANGED
|
@@ -40,15 +40,15 @@ graphddb generate docs --entry models.ts --out doc.md
|
|
|
40
40
|
`<dir>/index.md` (backward compatible). Either way the command prints a JSON
|
|
41
41
|
result (`{"status":"ok","outDir":…,"files":[…]}`) to stdout, where `files`
|
|
42
42
|
reports the written path(s).
|
|
43
|
-
- **`--
|
|
44
|
-
|
|
45
|
-
Patterns and Mutation Contracts sections; omit it and those sections
|
|
46
|
-
Defaults to the entry module.
|
|
43
|
+
- **`--contracts`** (optional) points at the module exporting your `contracts`
|
|
44
|
+
map (the `graphddb.publishQuery` / `graphddb.publishCommand` results). It powers
|
|
45
|
+
the Access Patterns and Mutation Contracts sections; omit it and those sections
|
|
46
|
+
are empty. Defaults to the entry module.
|
|
47
47
|
|
|
48
48
|
To also document your access patterns and mutations:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
graphddb generate docs --entry models.ts --
|
|
51
|
+
graphddb generate docs --entry models.ts --contracts contracts.ts --out docs/
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## Output sections
|
|
@@ -78,7 +78,7 @@ flag (PITR, table class, encryption) render as `as configured at deployment`.
|
|
|
78
78
|
| --- | --- | --- |
|
|
79
79
|
| `--entry <file>` | (required) | Entry module registering the entity models. |
|
|
80
80
|
| `--out <path>` | (required) | Output path. A `*.md` file is written verbatim at that path; any other value is a directory and `index.md` is written inside it. |
|
|
81
|
-
| `--
|
|
81
|
+
| `--contracts <file>` | entry module | Module exporting the `contracts` map for Access Patterns / Mutation Contracts. |
|
|
82
82
|
| `--format <format>` | `markdown` | Output format (only `markdown` today). |
|
|
83
83
|
| `--split <mode>` | `none` | `none` emits a single `index.md`. `entity` is reserved for a future per-entity split. |
|
|
84
84
|
| `--template-dir <dir>` | — | Directory of override `<partial>.hbs` templates. |
|