create-githolon 0.62.0 → 0.63.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-githolon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scaffold a Nomos domain package: the starter domain + compile config + live e2e. `npm create githolon my-app`.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -23,6 +23,45 @@ back structured) · `t.ref(Agg)` (an id-valued reference) ·
|
|
|
23
23
|
`t.hasMany(Child).via("parent")` (the virtual read side of a child's ref —
|
|
24
24
|
nothing stored on the parent). Add `.optional()` where absence is legal.
|
|
25
25
|
|
|
26
|
+
## Relationships: reference aggregates, don't hand-roll composite ids
|
|
27
|
+
|
|
28
|
+
When one aggregate points at another — a listing's primary image, its many
|
|
29
|
+
images, a membership row — reach for a REFERENCE, not a `primary:<listingId>` /
|
|
30
|
+
`media:<listingId>:<attId>` string convention. A `t.ref` stores the target's id
|
|
31
|
+
(the same bytes a string would), but it tells the compiler, the generated
|
|
32
|
+
client, and the next reader *what that id means* — so queries can traverse it,
|
|
33
|
+
the client types it as a link, and the ER graph renders it.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export const Attachment = aggregate("Attachment", { url: t.string().merge(Lww) });
|
|
37
|
+
|
|
38
|
+
export const Listing = aggregate("Listing", {
|
|
39
|
+
title: t.string().merge(Lww),
|
|
40
|
+
primaryImage: t.ref(Attachment), // 1:1 — "exactly one" is one field (re-set to change)
|
|
41
|
+
media: t.hasMany(() => ListingMedia).via("listing"), // 1:N — the virtual read side of the child's ref
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// M:N WITH EDGE DATA — a real join aggregate whose FKs are refs (typed + traversable), carrying role/order.
|
|
45
|
+
export const ListingMedia = aggregate("ListingMedia", {
|
|
46
|
+
listing: t.ref(Listing),
|
|
47
|
+
attachment: t.ref(Attachment),
|
|
48
|
+
role: t.enum(["gallery", "datasheet", "manual"] as const).merge(Lww),
|
|
49
|
+
order: t.int().merge(Lww),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pick the shape by cardinality: **1:1** is a `t.ref` field ("one" is structural,
|
|
54
|
+
no invariant needed); **1:N** is `t.hasMany(...).via("backref")` on the parent
|
|
55
|
+
(virtual — the child's `t.ref` back-edge is the only thing stored, and
|
|
56
|
+
`parent.add("children", child)` writes ONE event on the child); **M:N** is a
|
|
57
|
+
join aggregate with `t.ref` foreign keys (a real row when the edge carries data
|
|
58
|
+
like role/order). Traverse in a query by keying on the ref: `query("mediaByListing").key("listing").returns(ListingMedia)`.
|
|
59
|
+
|
|
60
|
+
A bidirectional pair (`Listing.hasMany(ListingMedia)` ⇄ `ListingMedia.ref(Listing)`)
|
|
61
|
+
is a declaration-order cycle — pass a THUNK on either side (`t.hasMany(() =>
|
|
62
|
+
ListingMedia)` / `t.ref(() => Listing)`) and it resolves lazily; the emitted law
|
|
63
|
+
is byte-identical to the eager form. Worked example: `examples/relations`.
|
|
64
|
+
|
|
26
65
|
## Value objects: named, composable structured values
|
|
27
66
|
|
|
28
67
|
When a field is a STRUCTURE (an address, a location, a money amount), don't
|