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.62.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