@entifix/core 0.5.0 → 0.7.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.
Files changed (80) hide show
  1. package/dist/command-palette/command-matching.d.ts +1 -1
  2. package/dist/command-palette/command-matching.js +1 -1
  3. package/dist/command-palette/command-matching.js.map +1 -1
  4. package/dist/command-palette/command-source.d.ts +3 -3
  5. package/dist/command-palette/command-source.js.map +1 -1
  6. package/dist/entity-definition/decorators/use-case/use-case.d.ts +1 -1
  7. package/dist/entity-definition/decorators/use-case/use-case.js +1 -1
  8. package/dist/entity-definition/decorators/use-case/use-case.js.map +1 -1
  9. package/dist/entity-definition/describe/describe-child-columns.d.ts +1 -1
  10. package/dist/entity-definition/describe/describe-child-columns.js +1 -1
  11. package/dist/entity-definition/describe/describe-child-columns.js.map +1 -1
  12. package/dist/entity-definition/describe/describe-entity-columns.d.ts +24 -0
  13. package/dist/entity-definition/describe/describe-entity-columns.d.ts.map +1 -1
  14. package/dist/entity-definition/describe/describe-entity-columns.js +36 -2
  15. package/dist/entity-definition/describe/describe-entity-columns.js.map +1 -1
  16. package/dist/entity-definition/describe/describe-entity-use-cases.d.ts +2 -1
  17. package/dist/entity-definition/describe/describe-entity-use-cases.d.ts.map +1 -1
  18. package/dist/entity-definition/describe/describe-entity-use-cases.js +2 -1
  19. package/dist/entity-definition/describe/describe-entity-use-cases.js.map +1 -1
  20. package/dist/entity-definition/helpers/entity-definition-helpers.d.ts +2 -2
  21. package/dist/entity-definition/helpers/entity-definition-helpers.js +2 -2
  22. package/dist/entity-definition/helpers/entity-definition-helpers.js.map +1 -1
  23. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts +17 -4
  24. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts.map +1 -1
  25. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js +1 -0
  26. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js.map +1 -1
  27. package/dist/entity-definition/meta-entities/meta-use-case/meta-use-case.d.ts +1 -1
  28. package/dist/entity-definition/meta-entities/meta-use-case/meta-use-case.js.map +1 -1
  29. package/dist/entity-definition/reconstruct/clone-entity-draft.d.ts +1 -1
  30. package/dist/entity-definition/reconstruct/clone-entity-draft.d.ts.map +1 -1
  31. package/dist/entity-definition/reconstruct/clone-entity-draft.js +5 -2
  32. package/dist/entity-definition/reconstruct/clone-entity-draft.js.map +1 -1
  33. package/dist/entity-definition/reconstruct/reconstruct-entity.js +1 -1
  34. package/dist/entity-definition/reconstruct/reconstruct-entity.js.map +1 -1
  35. package/dist/envelope/types.d.ts +5 -7
  36. package/dist/envelope/types.d.ts.map +1 -1
  37. package/dist/envelope/types.js.map +1 -1
  38. package/dist/hierarchy/hierarchy.d.ts +77 -0
  39. package/dist/hierarchy/hierarchy.d.ts.map +1 -0
  40. package/dist/hierarchy/hierarchy.js +165 -0
  41. package/dist/hierarchy/hierarchy.js.map +1 -0
  42. package/dist/hierarchy/index.d.ts +2 -0
  43. package/dist/hierarchy/index.d.ts.map +1 -0
  44. package/dist/hierarchy/index.js +3 -0
  45. package/dist/hierarchy/index.js.map +1 -0
  46. package/dist/http/session-cookies.d.ts +2 -3
  47. package/dist/http/session-cookies.d.ts.map +1 -1
  48. package/dist/http/session-cookies.js +2 -3
  49. package/dist/http/session-cookies.js.map +1 -1
  50. package/dist/i18n/locales.js +2 -2
  51. package/dist/i18n/locales.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/types/BulkOutcome.d.ts +1 -2
  57. package/dist/types/BulkOutcome.d.ts.map +1 -1
  58. package/dist/types/BulkOutcome.js.map +1 -1
  59. package/dist/types/EntityAction.d.ts +1 -2
  60. package/dist/types/EntityAction.d.ts.map +1 -1
  61. package/dist/types/EntityAction.js +1 -2
  62. package/dist/types/EntityAction.js.map +1 -1
  63. package/dist/types/EntityDraft.d.ts +2 -3
  64. package/dist/types/EntityDraft.d.ts.map +1 -1
  65. package/dist/types/EntityDraft.js.map +1 -1
  66. package/dist/types/EntityRowDraft.d.ts +2 -3
  67. package/dist/types/EntityRowDraft.d.ts.map +1 -1
  68. package/dist/types/EntityRowDraft.js +1 -1
  69. package/dist/types/EntityRowDraft.js.map +1 -1
  70. package/dist/types/EntitySelection.d.ts +2 -3
  71. package/dist/types/EntitySelection.d.ts.map +1 -1
  72. package/dist/types/EntitySelection.js.map +1 -1
  73. package/dist/wizard/wizard-definition.d.ts +5 -8
  74. package/dist/wizard/wizard-definition.d.ts.map +1 -1
  75. package/dist/wizard/wizard-definition.js +3 -5
  76. package/dist/wizard/wizard-definition.js.map +1 -1
  77. package/dist/wizard/wizard-state.d.ts +4 -7
  78. package/dist/wizard/wizard-state.d.ts.map +1 -1
  79. package/dist/wizard/wizard-state.js.map +1 -1
  80. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-accessor/meta-accessor.ts"],"sourcesContent":["export type MetaAccessorKind = 'getter' | 'setter';\n\n/**\n * The kinds of value an accessor can hold, as far as generic UI and adapters\n * care. It is deliberately a presentation/behaviour taxonomy rather than a\n * TypeScript mirror: `id` and `enum` are strings at runtime but are rendered,\n * sorted and filtered differently.\n *\n * `localized` holds a text per locale (`LocalizedText`). It is queried only\n * through one locale at a time — `name.en` — never as the whole object.\n */\nexport const MetaAccessorTypes = [\n 'string',\n 'number',\n 'boolean',\n 'date',\n 'enum',\n 'id',\n 'link',\n 'linkCollection',\n 'composition',\n 'scalarCollection',\n 'localized',\n] as const;\nexport type MetaAccessorType = (typeof MetaAccessorTypes)[number];\n\n/**\n * The class whose accessors describe one row of a `composition` member.\n *\n * Deliberately not `EntityConstructor`: a child of a composition is a **value**,\n * not an entity. It has no identity of its own, is never addressed apart from\n * the master that holds it, and therefore has no `id` for `Entity` to require.\n * What it does have is `@accessor()` metadata, which is written to its own\n * class's `Symbol.metadata` bag independently of `@entity()` — so a child is\n * described by its accessors and by nothing else.\n *\n * ⚠️ It is a **shape declaration, not a runtime constructor contract.** Children\n * arrive off the wire as plain objects — the serializer walks `instanceof\n * EntityLink`, not the declared type, so an embedded array passes straight\n * through — and ADR 0032 forbids a class instance inside an autosaved draft\n * anyway. Nothing may branch on `instanceof` against this constructor.\n */\nexport type ChildConstructor<TChild extends object = object> = new (\n // Mirrors `EntityConstructor`: the arguments are the child's business, only\n // the class identity matters here.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...args: any[]\n) => TChild;\n\n/**\n * The wire shape a relation takes: the target's foreign key, or the whole\n * target inlined. Declared here rather than next to `EntityLink` because it is\n * metadata about a member, not state of a link instance.\n */\nexport const EntityLinkSerializations = ['id', 'embedded'] as const;\nexport type EntityLinkSerialization = (typeof EntityLinkSerializations)[number];\n\nexport interface MetaAccessorOptions {\n alias?: string;\n type?: MetaAccessorType;\n readonly?: boolean;\n hidden?: boolean;\n /** Member must hold a value. Drives form validation; defaults to optional. */\n required?: boolean;\n /** Human label for UI. Falls back to a humanized `name`. */\n label?: string;\n /**\n * Translation key for the label, resolved by the presentation layer. Takes\n * precedence over `label`, which stays as the untranslated fallback for a\n * member that has no catalog entry yet.\n *\n * Keys mirror the entity's own `@entity({ key })`, so they are derivable\n * rather than invented: `entity:product-specification.fields.code`.\n */\n labelKey?: string;\n /** Opt in/out of sorting controls. Defaults per `type`. */\n sortable?: boolean;\n /** Opt in/out of filtering controls. Defaults per `type`. */\n filterable?: boolean;\n /** Default position among the entity's members; ties keep declaration order. */\n order?: number;\n /** Allowed values when `type` is `enum` — drives the filter value control. */\n enumValues?: readonly string[];\n /**\n * Catalog prefix for those values; a value's label is `${enumLabelKey}.${value}`.\n * Explicit rather than derived from `labelKey`, so a member whose values are\n * shared with another member can point both at one vocabulary.\n */\n enumLabelKey?: string;\n /** Property of a `link` target used as its display label. Default `name`. */\n linkLabelProperty?: string;\n /**\n * Property of a `link` target a picker searches on. Defaults to\n * {@link linkLabelProperty} — you search for what you read. It must be\n * `filterable` on the target entity, since member metadata is also the\n * server-side filter allowlist.\n */\n linkSearchProperty?: string;\n /**\n * How a `link` travels when its owner is serialized: as the target's scalar\n * foreign key (`id`, the default) or with the whole target inlined\n * (`embedded`). The serializer decides per *link instance* — it inlines\n * whatever `isLoaded` — so without this the shape a write takes would depend\n * on whether the form happened to hold the target. Declaring it on the entity\n * makes the wire shape a property of the relation instead of an accident of\n * the UI.\n */\n linkSerialization?: EntityLinkSerialization;\n /**\n * The class describing one row of a `composition` member — the child's own\n * `@accessor()` metadata is what a detail grid renders its columns from.\n *\n * A **thunk**, because a child module and the entity that owns it are usually\n * declared in the same package and a direct reference makes the decorator's\n * evaluation order load-bearing. Deferring the read to first use removes the\n * ordering question entirely, which is the same reason a to-one relation's\n * target is reached through a resolver rather than an import.\n *\n * Meaningless on any other type. There is no inference fallback: an empty\n * array cannot be told apart from an empty `string[]` at runtime, so a\n * collection must be declared.\n */\n childType?: () => ChildConstructor;\n /**\n * Reset this member when the record is copied.\n *\n * It lives on the **accessor**, not on a use case's descriptor, and that is a\n * decision already on file: ADR 0026 closed `UseCaseDescriptor` against\n * per-verb payloads and named this exact case, because a descriptor that\n * grows a field per verb ends up a union of every verb's arguments. Which\n * members a copy must not carry is a property of the *member* — a unique\n * code, an audit stamp, a sequence number — and it is the same answer however\n * the copy is triggered.\n *\n * The identity member is always reset and needs no flag.\n */\n resetOnClone?: boolean;\n /**\n * The fewest values a collection member may hold — `1` for \"not empty\". Only\n * meaningful on a collection type (`scalarCollection`, `linkCollection`,\n * `composition`); declaring it elsewhere fails when the entity is described.\n *\n * Distinct from {@link required}: on a collection `required` already means\n * \"at least one\", and a `minItems` of two is a fact `required` cannot state.\n */\n minItems?: number;\n /** The most values a collection member may hold. Collection types only. */\n maxItems?: number;\n /**\n * Another `date` member this one may not precede — a period's `end` declares\n * `notBefore: 'start'`. Checked only when both hold a value. Both members must\n * be `date`s, or describing the entity fails.\n */\n notBefore?: string;\n}\n\nexport class MetaAccessor {\n //#region Properties\n\n readonly name: string | symbol;\n readonly kind: MetaAccessorKind;\n readonly alias?: string;\n readonly type?: MetaAccessorType;\n readonly readonly?: boolean;\n readonly hidden?: boolean;\n readonly required?: boolean;\n readonly label?: string;\n readonly labelKey?: string;\n readonly sortable?: boolean;\n readonly filterable?: boolean;\n readonly order?: number;\n readonly enumValues?: readonly string[];\n readonly enumLabelKey?: string;\n readonly linkLabelProperty?: string;\n readonly linkSearchProperty?: string;\n readonly linkSerialization?: EntityLinkSerialization;\n readonly childType?: () => ChildConstructor;\n readonly resetOnClone?: boolean;\n readonly minItems?: number;\n readonly maxItems?: number;\n readonly notBefore?: string;\n\n //#endregion\n\n //#region Constructors\n constructor(\n name: string | symbol,\n kind: MetaAccessorKind,\n options?: MetaAccessorOptions,\n ) {\n this.name = name;\n this.kind = kind;\n this.alias = options?.alias;\n this.type = options?.type;\n this.readonly = options?.readonly;\n this.hidden = options?.hidden;\n this.required = options?.required;\n this.label = options?.label;\n this.labelKey = options?.labelKey;\n this.sortable = options?.sortable;\n this.filterable = options?.filterable;\n this.order = options?.order;\n this.enumValues = options?.enumValues;\n this.enumLabelKey = options?.enumLabelKey;\n this.linkLabelProperty = options?.linkLabelProperty;\n this.linkSearchProperty = options?.linkSearchProperty;\n this.linkSerialization = options?.linkSerialization;\n this.childType = options?.childType;\n this.resetOnClone = options?.resetOnClone;\n this.minItems = options?.minItems;\n this.maxItems = options?.maxItems;\n this.notBefore = options?.notBefore;\n }\n //#endregion\n\n //#region Methods\n //#endregion\n\n //#region Accessors\n //#endregion\n}\n"],"names":["MetaAccessorTypes","EntityLinkSerializations","MetaAccessor","name","kind","options","alias","type","readonly","hidden","required","label","labelKey","sortable","filterable","order","enumValues","enumLabelKey","linkLabelProperty","linkSearchProperty","linkSerialization","childType","resetOnClone","minItems","maxItems","notBefore"],"mappings":"AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMA,oBAAoB;IAC/B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AA0BX;;;;CAIC,GACD,OAAO,MAAMC,2BAA2B;IAAC;IAAM;CAAW,CAAU;AAsGpE,OAAO,MAAMC;IA0BX,YAAY;IAEZ,sBAAsB;IACtB,YACEC,IAAqB,EACrBC,IAAsB,EACtBC,OAA6B,CAC7B;QACA,IAAI,CAACF,IAAI,GAAGA;QACZ,IAAI,CAACC,IAAI,GAAGA;QACZ,IAAI,CAACE,KAAK,GAAGD,2BAAAA,QAASC,KAAK;QAC3B,IAAI,CAACC,IAAI,GAAGF,2BAAAA,QAASE,IAAI;QACzB,IAAI,CAACC,QAAQ,GAAGH,2BAAAA,QAASG,QAAQ;QACjC,IAAI,CAACC,MAAM,GAAGJ,2BAAAA,QAASI,MAAM;QAC7B,IAAI,CAACC,QAAQ,GAAGL,2BAAAA,QAASK,QAAQ;QACjC,IAAI,CAACC,KAAK,GAAGN,2BAAAA,QAASM,KAAK;QAC3B,IAAI,CAACC,QAAQ,GAAGP,2BAAAA,QAASO,QAAQ;QACjC,IAAI,CAACC,QAAQ,GAAGR,2BAAAA,QAASQ,QAAQ;QACjC,IAAI,CAACC,UAAU,GAAGT,2BAAAA,QAASS,UAAU;QACrC,IAAI,CAACC,KAAK,GAAGV,2BAAAA,QAASU,KAAK;QAC3B,IAAI,CAACC,UAAU,GAAGX,2BAAAA,QAASW,UAAU;QACrC,IAAI,CAACC,YAAY,GAAGZ,2BAAAA,QAASY,YAAY;QACzC,IAAI,CAACC,iBAAiB,GAAGb,2BAAAA,QAASa,iBAAiB;QACnD,IAAI,CAACC,kBAAkB,GAAGd,2BAAAA,QAASc,kBAAkB;QACrD,IAAI,CAACC,iBAAiB,GAAGf,2BAAAA,QAASe,iBAAiB;QACnD,IAAI,CAACC,SAAS,GAAGhB,2BAAAA,QAASgB,SAAS;QACnC,IAAI,CAACC,YAAY,GAAGjB,2BAAAA,QAASiB,YAAY;QACzC,IAAI,CAACC,QAAQ,GAAGlB,2BAAAA,QAASkB,QAAQ;QACjC,IAAI,CAACC,QAAQ,GAAGnB,2BAAAA,QAASmB,QAAQ;QACjC,IAAI,CAACC,SAAS,GAAGpB,2BAAAA,QAASoB,SAAS;IACrC;AAQF"}
1
+ {"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-accessor/meta-accessor.ts"],"sourcesContent":["export type MetaAccessorKind = 'getter' | 'setter';\n\n/**\n * The kinds of value an accessor can hold, as far as generic UI and adapters\n * care. It is deliberately a presentation/behaviour taxonomy rather than a\n * TypeScript mirror: `id` and `enum` are strings at runtime but are rendered,\n * sorted and filtered differently.\n *\n * `localized` holds a text per locale (`LocalizedText`). It is queried only\n * through one locale at a time — `name.en` — never as the whole object.\n */\nexport const MetaAccessorTypes = [\n 'string',\n 'number',\n 'boolean',\n 'date',\n 'enum',\n 'id',\n 'link',\n 'linkCollection',\n 'composition',\n 'scalarCollection',\n 'localized',\n] as const;\nexport type MetaAccessorType = (typeof MetaAccessorTypes)[number];\n\n/**\n * The class whose accessors describe one row of a `composition` member.\n *\n * Deliberately not `EntityConstructor`: a child of a composition is a **value**,\n * not an entity. It has no identity of its own, is never addressed apart from\n * the master that holds it, and therefore has no `id` for `Entity` to require.\n * What it does have is `@accessor()` metadata, which is written to its own\n * class's `Symbol.metadata` bag independently of `@entity()` — so a child is\n * described by its accessors and by nothing else.\n *\n * ⚠️ It is a **shape declaration, not a runtime constructor contract.** Children\n * arrive off the wire as plain objects — the serializer walks `instanceof\n * EntityLink`, not the declared type, so an embedded array passes straight\n * through — and an autosaved draft may hold nothing that does not survive a\n * JSON round trip anyway. Nothing may branch on `instanceof` against this constructor.\n */\nexport type ChildConstructor<TChild extends object = object> = new (\n // Mirrors `EntityConstructor`: the arguments are the child's business, only\n // the class identity matters here.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...args: any[]\n) => TChild;\n\n/**\n * The wire shape a relation takes: the target's foreign key, or the whole\n * target inlined. Declared here rather than next to `EntityLink` because it is\n * metadata about a member, not state of a link instance.\n */\nexport const EntityLinkSerializations = ['id', 'embedded'] as const;\nexport type EntityLinkSerialization = (typeof EntityLinkSerializations)[number];\n\nexport interface MetaAccessorOptions {\n alias?: string;\n type?: MetaAccessorType;\n readonly?: boolean;\n hidden?: boolean;\n /** Member must hold a value. Drives form validation; defaults to optional. */\n required?: boolean;\n /** Human label for UI. Falls back to a humanized `name`. */\n label?: string;\n /**\n * Translation key for the label, resolved by the presentation layer. Takes\n * precedence over `label`, which stays as the untranslated fallback for a\n * member that has no catalog entry yet.\n *\n * Keys mirror the entity's own `@entity({ key })`, so they are derivable\n * rather than invented: `entity:product-specification.fields.code`.\n */\n labelKey?: string;\n /** Opt in/out of sorting controls. Defaults per `type`. */\n sortable?: boolean;\n /** Opt in/out of filtering controls. Defaults per `type`. */\n filterable?: boolean;\n /** Default position among the entity's members; ties keep declaration order. */\n order?: number;\n /** Allowed values when `type` is `enum` — drives the filter value control. */\n enumValues?: readonly string[];\n /**\n * Catalog prefix for those values; a value's label is `${enumLabelKey}.${value}`.\n * Explicit rather than derived from `labelKey`, so a member whose values are\n * shared with another member can point both at one vocabulary.\n */\n enumLabelKey?: string;\n /** Property of a `link` target used as its display label. Default `name`. */\n linkLabelProperty?: string;\n /**\n * Property of a `link` target a picker searches on. Defaults to\n * {@link linkLabelProperty} — you search for what you read. It must be\n * `filterable` on the target entity, since member metadata is also the\n * server-side filter allowlist.\n */\n linkSearchProperty?: string;\n /**\n * How a `link` travels when its owner is serialized: as the target's scalar\n * foreign key (`id`, the default) or with the whole target inlined\n * (`embedded`). The serializer decides per *link instance* — it inlines\n * whatever `isLoaded` — so without this the shape a write takes would depend\n * on whether the form happened to hold the target. Declaring it on the entity\n * makes the wire shape a property of the relation instead of an accident of\n * the UI.\n */\n linkSerialization?: EntityLinkSerialization;\n /**\n * The class describing one row of a `composition` member — the child's own\n * `@accessor()` metadata is what a detail grid renders its columns from.\n *\n * A **thunk**, because a child module and the entity that owns it are usually\n * declared in the same package and a direct reference makes the decorator's\n * evaluation order load-bearing. Deferring the read to first use removes the\n * ordering question entirely, which is the same reason a to-one relation's\n * target is reached through a resolver rather than an import.\n *\n * Meaningless on any other type. There is no inference fallback: an empty\n * array cannot be told apart from an empty `string[]` at runtime, so a\n * collection must be declared.\n */\n childType?: () => ChildConstructor;\n /**\n * Reset this member when the record is copied.\n *\n * It lives on the **accessor**, not on a use case's descriptor, and that is a\n * decision already made: `UseCaseDescriptor` is closed against per-verb\n * payloads, and this is the exact case, because a descriptor that\n * grows a field per verb ends up a union of every verb's arguments. Which\n * members a copy must not carry is a property of the *member* — a unique\n * code, an audit stamp, a sequence number — and it is the same answer however\n * the copy is triggered.\n *\n * The identity member is always reset and needs no flag.\n */\n resetOnClone?: boolean;\n /**\n * The fewest values a collection member may hold — `1` for \"not empty\". Only\n * meaningful on a collection type (`scalarCollection`, `linkCollection`,\n * `composition`); declaring it elsewhere fails when the entity is described.\n *\n * Distinct from {@link required}: on a collection `required` already means\n * \"at least one\", and a `minItems` of two is a fact `required` cannot state.\n */\n minItems?: number;\n /** The most values a collection member may hold. Collection types only. */\n maxItems?: number;\n /**\n * Another `date` member this one may not precede — a period's `end` declares\n * `notBefore: 'start'`. Checked only when both hold a value. Both members must\n * be `date`s, or describing the entity fails.\n */\n notBefore?: string;\n /**\n * This member holds the id of the record's **parent**, another record of\n * the same entity — so the entity forms a tree. At most one member per\n * entity, and it must be an `id` or a `string`; describing the entity fails\n * otherwise.\n *\n * An id rather than a `link` on purpose. A tree is always built from the\n * whole set of its records ({@link buildTree}), never by resolving one parent\n * at a time, so a link's lazy resolution would be a fetch per node that\n * nothing needs. Absent means a root.\n */\n parent?: boolean;\n}\n\nexport class MetaAccessor {\n //#region Properties\n\n readonly name: string | symbol;\n readonly kind: MetaAccessorKind;\n readonly alias?: string;\n readonly type?: MetaAccessorType;\n readonly readonly?: boolean;\n readonly hidden?: boolean;\n readonly required?: boolean;\n readonly label?: string;\n readonly labelKey?: string;\n readonly sortable?: boolean;\n readonly filterable?: boolean;\n readonly order?: number;\n readonly enumValues?: readonly string[];\n readonly enumLabelKey?: string;\n readonly linkLabelProperty?: string;\n readonly linkSearchProperty?: string;\n readonly linkSerialization?: EntityLinkSerialization;\n readonly childType?: () => ChildConstructor;\n readonly resetOnClone?: boolean;\n readonly minItems?: number;\n readonly maxItems?: number;\n readonly notBefore?: string;\n readonly parent?: boolean;\n\n //#endregion\n\n //#region Constructors\n constructor(\n name: string | symbol,\n kind: MetaAccessorKind,\n options?: MetaAccessorOptions,\n ) {\n this.name = name;\n this.kind = kind;\n this.alias = options?.alias;\n this.type = options?.type;\n this.readonly = options?.readonly;\n this.hidden = options?.hidden;\n this.required = options?.required;\n this.label = options?.label;\n this.labelKey = options?.labelKey;\n this.sortable = options?.sortable;\n this.filterable = options?.filterable;\n this.order = options?.order;\n this.enumValues = options?.enumValues;\n this.enumLabelKey = options?.enumLabelKey;\n this.linkLabelProperty = options?.linkLabelProperty;\n this.linkSearchProperty = options?.linkSearchProperty;\n this.linkSerialization = options?.linkSerialization;\n this.childType = options?.childType;\n this.resetOnClone = options?.resetOnClone;\n this.minItems = options?.minItems;\n this.maxItems = options?.maxItems;\n this.notBefore = options?.notBefore;\n this.parent = options?.parent;\n }\n //#endregion\n\n //#region Methods\n //#endregion\n\n //#region Accessors\n //#endregion\n}\n"],"names":["MetaAccessorTypes","EntityLinkSerializations","MetaAccessor","name","kind","options","alias","type","readonly","hidden","required","label","labelKey","sortable","filterable","order","enumValues","enumLabelKey","linkLabelProperty","linkSearchProperty","linkSerialization","childType","resetOnClone","minItems","maxItems","notBefore","parent"],"mappings":"AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMA,oBAAoB;IAC/B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AA0BX;;;;CAIC,GACD,OAAO,MAAMC,2BAA2B;IAAC;IAAM;CAAW,CAAU;AAkHpE,OAAO,MAAMC;IA2BX,YAAY;IAEZ,sBAAsB;IACtB,YACEC,IAAqB,EACrBC,IAAsB,EACtBC,OAA6B,CAC7B;QACA,IAAI,CAACF,IAAI,GAAGA;QACZ,IAAI,CAACC,IAAI,GAAGA;QACZ,IAAI,CAACE,KAAK,GAAGD,2BAAAA,QAASC,KAAK;QAC3B,IAAI,CAACC,IAAI,GAAGF,2BAAAA,QAASE,IAAI;QACzB,IAAI,CAACC,QAAQ,GAAGH,2BAAAA,QAASG,QAAQ;QACjC,IAAI,CAACC,MAAM,GAAGJ,2BAAAA,QAASI,MAAM;QAC7B,IAAI,CAACC,QAAQ,GAAGL,2BAAAA,QAASK,QAAQ;QACjC,IAAI,CAACC,KAAK,GAAGN,2BAAAA,QAASM,KAAK;QAC3B,IAAI,CAACC,QAAQ,GAAGP,2BAAAA,QAASO,QAAQ;QACjC,IAAI,CAACC,QAAQ,GAAGR,2BAAAA,QAASQ,QAAQ;QACjC,IAAI,CAACC,UAAU,GAAGT,2BAAAA,QAASS,UAAU;QACrC,IAAI,CAACC,KAAK,GAAGV,2BAAAA,QAASU,KAAK;QAC3B,IAAI,CAACC,UAAU,GAAGX,2BAAAA,QAASW,UAAU;QACrC,IAAI,CAACC,YAAY,GAAGZ,2BAAAA,QAASY,YAAY;QACzC,IAAI,CAACC,iBAAiB,GAAGb,2BAAAA,QAASa,iBAAiB;QACnD,IAAI,CAACC,kBAAkB,GAAGd,2BAAAA,QAASc,kBAAkB;QACrD,IAAI,CAACC,iBAAiB,GAAGf,2BAAAA,QAASe,iBAAiB;QACnD,IAAI,CAACC,SAAS,GAAGhB,2BAAAA,QAASgB,SAAS;QACnC,IAAI,CAACC,YAAY,GAAGjB,2BAAAA,QAASiB,YAAY;QACzC,IAAI,CAACC,QAAQ,GAAGlB,2BAAAA,QAASkB,QAAQ;QACjC,IAAI,CAACC,QAAQ,GAAGnB,2BAAAA,QAASmB,QAAQ;QACjC,IAAI,CAACC,SAAS,GAAGpB,2BAAAA,QAASoB,SAAS;QACnC,IAAI,CAACC,MAAM,GAAGrB,2BAAAA,QAASqB,MAAM;IAC/B;AAQF"}
@@ -56,7 +56,7 @@ export interface MetaUseCaseOptions<TKey extends string, TEntity extends Entity>
56
56
  * refuses anyway, since a verb key is the third segment of one permission and
57
57
  * two classes cannot share a key. Three classes would have meant three
58
58
  * permissions for one act, and a grant that let somebody publish one offering
59
- * but not twenty (#216).
59
+ * but not twenty.
60
60
  *
61
61
  * The primary cell stays where it is so that nothing already declared
62
62
  * changes, and so a descriptor still reads as "this verb lives here" with the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-use-case/meta-use-case.ts"],"sourcesContent":["import type { Entity, EntityConstructor } from '../../../types/Entity.js';\n\n/** What a use case acts on: one record, a selection, or nothing. */\nexport type UseCaseBinding = 'entity' | 'collection' | 'unbound';\n\n/**\n * Where a surface puts the action. Deliberately **not** derivable from\n * {@link UseCaseBinding}: an entity-bound action can be determining (a footer\n * \"Publish\" that finalizes the page) or context-independent (a toolbar action\n * available whenever a record is open). Collapsing the two would force every\n * surface to re-derive a placement the author already knew.\n */\nexport type UseCasePlacement =\n 'context-dependent' | 'context-independent' | 'determining';\n\n/**\n * One place a verb can appear: a binding and a placement together.\n *\n * The pair is the unit rather than either half, because the two answer\n * different questions — placement decides the *surface* and binding decides the\n * *payload* — and a verb reachable from a row and from a selection differs in\n * both at once.\n */\nexport interface UseCaseCell {\n binding: UseCaseBinding;\n placement: UseCasePlacement;\n}\n\n/** Confirmation a surface must obtain before running the use case. */\nexport interface UseCaseConfirm {\n tone: 'destructive' | 'neutral';\n /** Catalog key. Copy never lives in a descriptor. */\n messageKey: string;\n}\n\n/**\n * What `@useCase()` declares.\n *\n * `entity` is the target the verb is registered against — it is what the\n * decorator appends to, and it is not part of the served descriptor.\n *\n * `labelKey` is mandatory and namespace-qualified, the `GuardedNavItem`\n * convention: i18n is mandatory and `react/jsx-no-literals` fails the build on a\n * string written into JSX, so a descriptor carrying copy could not be rendered.\n *\n * `keywordsKey` is a catalog key rather than a `string[]` so the command palette\n * matches across locales — a user typing an English term reaches a Spanish\n * command.\n */\nexport interface MetaUseCaseOptions<\n TKey extends string,\n TEntity extends Entity,\n> {\n entity: EntityConstructor<TEntity>;\n key: TKey;\n binding: UseCaseBinding;\n placement: UseCasePlacement;\n /**\n * Further places the same verb appears, beyond the {@link binding} and\n * {@link placement} above.\n *\n * ⚠️ **One verb, one permission, several surfaces.** Publishing an offering\n * from its form, from a row menu and over a selection is one act reached three\n * ways, and the alternative was three `@useCase()` classes — which `slices`\n * refuses anyway, since a verb key is the third segment of one permission and\n * two classes cannot share a key. Three classes would have meant three\n * permissions for one act, and a grant that let somebody publish one offering\n * but not twenty (#216).\n *\n * The primary cell stays where it is so that nothing already declared\n * changes, and so a descriptor still reads as \"this verb lives here\" with the\n * rest as additions.\n */\n alsoAt?: readonly UseCaseCell[];\n labelKey: string;\n keywordsKey?: string;\n confirm?: UseCaseConfirm;\n /** Names a form a surface should open instead of acting immediately. */\n form?: string;\n}\n\n/**\n * What the use-case class's **own** metadata carries, as opposed to what is\n * appended to the entity. Two fields are enough to derive the permission, which\n * is what lets `permissionForUseCase(SomeUC)` take a single argument and keeps\n * the verb string written exactly once.\n */\nexport interface MetaUseCaseBinding {\n entity: EntityConstructor<Entity>;\n key: string;\n}\n\nexport class MetaUseCase {\n //#region Properties\n readonly entity: EntityConstructor<Entity>;\n readonly key: string;\n readonly binding: UseCaseBinding;\n readonly placement: UseCasePlacement;\n readonly alsoAt?: readonly UseCaseCell[];\n readonly labelKey: string;\n readonly keywordsKey?: string;\n readonly confirm?: UseCaseConfirm;\n readonly form?: string;\n //#endregion\n\n //#region Constructors\n constructor(options: MetaUseCaseOptions<string, Entity>) {\n this.entity = options.entity;\n this.key = options.key;\n this.binding = options.binding;\n this.placement = options.placement;\n this.alsoAt = options.alsoAt;\n this.labelKey = options.labelKey;\n this.keywordsKey = options.keywordsKey;\n this.confirm = options.confirm;\n this.form = options.form;\n }\n //#endregion\n\n //#region Methods\n //#endregion\n\n //#region Accessors\n //#endregion\n}\n"],"names":["MetaUseCase","options","entity","key","binding","placement","alsoAt","labelKey","keywordsKey","confirm","form"],"mappings":"AA4FA,OAAO,MAAMA;IAWX,YAAY;IAEZ,sBAAsB;IACtB,YAAYC,OAA2C,CAAE;QACvD,IAAI,CAACC,MAAM,GAAGD,QAAQC,MAAM;QAC5B,IAAI,CAACC,GAAG,GAAGF,QAAQE,GAAG;QACtB,IAAI,CAACC,OAAO,GAAGH,QAAQG,OAAO;QAC9B,IAAI,CAACC,SAAS,GAAGJ,QAAQI,SAAS;QAClC,IAAI,CAACC,MAAM,GAAGL,QAAQK,MAAM;QAC5B,IAAI,CAACC,QAAQ,GAAGN,QAAQM,QAAQ;QAChC,IAAI,CAACC,WAAW,GAAGP,QAAQO,WAAW;QACtC,IAAI,CAACC,OAAO,GAAGR,QAAQQ,OAAO;QAC9B,IAAI,CAACC,IAAI,GAAGT,QAAQS,IAAI;IAC1B;AAQF"}
1
+ {"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-use-case/meta-use-case.ts"],"sourcesContent":["import type { Entity, EntityConstructor } from '../../../types/Entity.js';\n\n/** What a use case acts on: one record, a selection, or nothing. */\nexport type UseCaseBinding = 'entity' | 'collection' | 'unbound';\n\n/**\n * Where a surface puts the action. Deliberately **not** derivable from\n * {@link UseCaseBinding}: an entity-bound action can be determining (a footer\n * \"Publish\" that finalizes the page) or context-independent (a toolbar action\n * available whenever a record is open). Collapsing the two would force every\n * surface to re-derive a placement the author already knew.\n */\nexport type UseCasePlacement =\n 'context-dependent' | 'context-independent' | 'determining';\n\n/**\n * One place a verb can appear: a binding and a placement together.\n *\n * The pair is the unit rather than either half, because the two answer\n * different questions — placement decides the *surface* and binding decides the\n * *payload* — and a verb reachable from a row and from a selection differs in\n * both at once.\n */\nexport interface UseCaseCell {\n binding: UseCaseBinding;\n placement: UseCasePlacement;\n}\n\n/** Confirmation a surface must obtain before running the use case. */\nexport interface UseCaseConfirm {\n tone: 'destructive' | 'neutral';\n /** Catalog key. Copy never lives in a descriptor. */\n messageKey: string;\n}\n\n/**\n * What `@useCase()` declares.\n *\n * `entity` is the target the verb is registered against — it is what the\n * decorator appends to, and it is not part of the served descriptor.\n *\n * `labelKey` is mandatory and namespace-qualified, the `GuardedNavItem`\n * convention: i18n is mandatory and `react/jsx-no-literals` fails the build on a\n * string written into JSX, so a descriptor carrying copy could not be rendered.\n *\n * `keywordsKey` is a catalog key rather than a `string[]` so the command palette\n * matches across locales — a user typing an English term reaches a Spanish\n * command.\n */\nexport interface MetaUseCaseOptions<\n TKey extends string,\n TEntity extends Entity,\n> {\n entity: EntityConstructor<TEntity>;\n key: TKey;\n binding: UseCaseBinding;\n placement: UseCasePlacement;\n /**\n * Further places the same verb appears, beyond the {@link binding} and\n * {@link placement} above.\n *\n * ⚠️ **One verb, one permission, several surfaces.** Publishing an offering\n * from its form, from a row menu and over a selection is one act reached three\n * ways, and the alternative was three `@useCase()` classes — which `slices`\n * refuses anyway, since a verb key is the third segment of one permission and\n * two classes cannot share a key. Three classes would have meant three\n * permissions for one act, and a grant that let somebody publish one offering\n * but not twenty.\n *\n * The primary cell stays where it is so that nothing already declared\n * changes, and so a descriptor still reads as \"this verb lives here\" with the\n * rest as additions.\n */\n alsoAt?: readonly UseCaseCell[];\n labelKey: string;\n keywordsKey?: string;\n confirm?: UseCaseConfirm;\n /** Names a form a surface should open instead of acting immediately. */\n form?: string;\n}\n\n/**\n * What the use-case class's **own** metadata carries, as opposed to what is\n * appended to the entity. Two fields are enough to derive the permission, which\n * is what lets `permissionForUseCase(SomeUC)` take a single argument and keeps\n * the verb string written exactly once.\n */\nexport interface MetaUseCaseBinding {\n entity: EntityConstructor<Entity>;\n key: string;\n}\n\nexport class MetaUseCase {\n //#region Properties\n readonly entity: EntityConstructor<Entity>;\n readonly key: string;\n readonly binding: UseCaseBinding;\n readonly placement: UseCasePlacement;\n readonly alsoAt?: readonly UseCaseCell[];\n readonly labelKey: string;\n readonly keywordsKey?: string;\n readonly confirm?: UseCaseConfirm;\n readonly form?: string;\n //#endregion\n\n //#region Constructors\n constructor(options: MetaUseCaseOptions<string, Entity>) {\n this.entity = options.entity;\n this.key = options.key;\n this.binding = options.binding;\n this.placement = options.placement;\n this.alsoAt = options.alsoAt;\n this.labelKey = options.labelKey;\n this.keywordsKey = options.keywordsKey;\n this.confirm = options.confirm;\n this.form = options.form;\n }\n //#endregion\n\n //#region Methods\n //#endregion\n\n //#region Accessors\n //#endregion\n}\n"],"names":["MetaUseCase","options","entity","key","binding","placement","alsoAt","labelKey","keywordsKey","confirm","form"],"mappings":"AA4FA,OAAO,MAAMA;IAWX,YAAY;IAEZ,sBAAsB;IACtB,YAAYC,OAA2C,CAAE;QACvD,IAAI,CAACC,MAAM,GAAGD,QAAQC,MAAM;QAC5B,IAAI,CAACC,GAAG,GAAGF,QAAQE,GAAG;QACtB,IAAI,CAACC,OAAO,GAAGH,QAAQG,OAAO;QAC9B,IAAI,CAACC,SAAS,GAAGJ,QAAQI,SAAS;QAClC,IAAI,CAACC,MAAM,GAAGL,QAAQK,MAAM;QAC5B,IAAI,CAACC,QAAQ,GAAGN,QAAQM,QAAQ;QAChC,IAAI,CAACC,WAAW,GAAGP,QAAQO,WAAW;QACtC,IAAI,CAACC,OAAO,GAAGR,QAAQQ,OAAO;QAC9B,IAAI,CAACC,IAAI,GAAGT,QAAQS,IAAI;IAC1B;AAQF"}
@@ -7,7 +7,7 @@ import { EntityDraft } from '../../types/EntityDraft.js';
7
7
  * **A draft in and a draft out**, rather than an instance in and an instance
8
8
  * out, and that is the whole reason this composes with the rest of the form
9
9
  * layer. A form's state is a `Record<string, string>` that a workspace
10
- * autosaves (ADR 0032), so a clone that returned an instance would have to be
10
+ * autosaves, so a clone that returned an instance would have to be
11
11
  * re-seeded back into a draft by the caller — through `seedFieldValue`, which
12
12
  * lives in a package above this one. Staying in the draft keeps the operation
13
13
  * on one side of that boundary, and `reconstructEntity` is still what turns the
@@ -1 +1 @@
1
- {"version":3,"file":"clone-entity-draft.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/clone-entity-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAyB,MAAM,4BAA4B,CAAC;AAWhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,WAAW,GAClB,WAAW,CA0Bb"}
1
+ {"version":3,"file":"clone-entity-draft.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/clone-entity-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAyB,MAAM,4BAA4B,CAAC;AAWhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,WAAW,GAClB,WAAW,CA6Bb"}
@@ -9,7 +9,7 @@ import { describeEntityColumns } from '../describe/index.js';
9
9
  * **A draft in and a draft out**, rather than an instance in and an instance
10
10
  * out, and that is the whole reason this composes with the rest of the form
11
11
  * layer. A form's state is a `Record<string, string>` that a workspace
12
- * autosaves (ADR 0032), so a clone that returned an instance would have to be
12
+ * autosaves, so a clone that returned an instance would have to be
13
13
  * re-seeded back into a draft by the caller — through `seedFieldValue`, which
14
14
  * lives in a package above this one. Staying in the draft keeps the operation
15
15
  * on one side of that boundary, and `reconstructEntity` is still what turns the
@@ -46,7 +46,10 @@ import { describeEntityColumns } from '../describe/index.js';
46
46
  cloned[descriptor.name] = clonedRows(cloned[descriptor.name], descriptor.resetOnClone);
47
47
  continue;
48
48
  }
49
- if (descriptor.type !== 'id' && !descriptor.resetOnClone) continue;
49
+ // An `id` member is reset unless it is the parent: a copy of a node is its
50
+ // sibling, and a copy that came out as a root would be moved, not copied.
51
+ const isIdentityLike = descriptor.type === 'id' && !descriptor.parent;
52
+ if (!isIdentityLike && !descriptor.resetOnClone) continue;
50
53
  if (!(descriptor.name in cloned)) continue;
51
54
  cloned[descriptor.name] = '';
52
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/entity-definition/reconstruct/clone-entity-draft.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport { EntityDraft, type EntityDraftValue } from '../../types/EntityDraft.js';\nimport {\n newRowKey,\n readRowDrafts,\n ROW_KEY,\n} from '../../types/EntityRowDraft.js';\nimport { describeEntityColumns } from '../describe/index.js';\n\n/** The member `Entity` requires every record to carry. */\nconst ID_MEMBER = 'id';\n\n/**\n * The draft for a copy of a record: everything the original held, minus the\n * members a copy must not carry.\n *\n * **A draft in and a draft out**, rather than an instance in and an instance\n * out, and that is the whole reason this composes with the rest of the form\n * layer. A form's state is a `Record<string, string>` that a workspace\n * autosaves (ADR 0032), so a clone that returned an instance would have to be\n * re-seeded back into a draft by the caller — through `seedFieldValue`, which\n * lives in a package above this one. Staying in the draft keeps the operation\n * on one side of that boundary, and `reconstructEntity` is still what turns the\n * result into an entity at submit.\n *\n * Two kinds of member are dropped:\n *\n * - **the identity member**, always, with no flag — a copy that carries the\n * original's `id` is an update to the original, which is the one outcome a\n * Clone button must never produce;\n * - **anything declared `@accessor({ resetOnClone: true })`** — a unique code,\n * an audit stamp, a sequence number.\n *\n * Cleared to `''` rather than deleted, because `''` is what an empty control\n * drafts as: a member removed from the record entirely would arrive\n * `undefined`, and an input handed `undefined` flips from controlled to\n * uncontrolled — the same trap `restoreEntityDraft` layers a restored draft\n * over a seeded one to avoid.\n *\n * A member the entity declares but the draft never held stays absent, so this\n * neither invents keys nor widens the draft.\n */\nexport function cloneEntityDraft<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n): EntityDraft {\n const cloned: EntityDraft = { ...values };\n\n // The identity member, cleared **without consulting the descriptors**.\n // `describeEntityColumns` drops `hidden` members, and a form hiding its id is\n // the ordinary case rather than the exotic one — every generated catalog page\n // does it — so a descriptor-driven sweep would leave the id in place on\n // exactly the forms a Clone button appears on, and the \"copy\" would silently\n // save over the original. `Entity` fixes this member's name, so there is no\n // metadata to consult anyway.\n if (ID_MEMBER in cloned) cloned[ID_MEMBER] = '';\n\n for (const descriptor of describeEntityColumns(entityConstructor)) {\n if (descriptor.type === 'composition') {\n cloned[descriptor.name] = clonedRows(\n cloned[descriptor.name],\n descriptor.resetOnClone,\n );\n continue;\n }\n if (descriptor.type !== 'id' && !descriptor.resetOnClone) continue;\n if (!(descriptor.name in cloned)) continue;\n cloned[descriptor.name] = '';\n }\n\n return cloned;\n}\n\n/**\n * The rows a copy starts with.\n *\n * Two things the `''` rule above cannot express. A collection is **emptied to\n * `[]`, never to `''`**: an empty string is not a readable row list, so\n * `reconstructEntity` would skip the member and the \"reset\" collection would\n * come back holding the original's lines — a `resetOnClone` that resets nothing.\n *\n * And a row that *is* copied gets a **fresh key**. The key identifies a row\n * within one editing session; carrying the original's over means the copy and\n * the original name their lines identically, so a workspace holding both open\n * has two tabs whose rows claim the same identity — harmless until anything\n * addresses a row across them, and impossible to debug once it does.\n */\nfunction clonedRows(\n value: EntityDraftValue | undefined,\n reset: boolean,\n): EntityDraftValue {\n if (reset) return [];\n return readRowDrafts(value).map(row => ({ ...row, [ROW_KEY]: newRowKey() }));\n}\n"],"names":["newRowKey","readRowDrafts","ROW_KEY","describeEntityColumns","ID_MEMBER","cloneEntityDraft","entityConstructor","values","cloned","descriptor","type","name","clonedRows","resetOnClone","value","reset","map","row"],"mappings":";AAEA,SACEA,SAAS,EACTC,aAAa,EACbC,OAAO,QACF,gCAAgC;AACvC,SAASC,qBAAqB,QAAQ,uBAAuB;AAE7D,wDAAwD,GACxD,MAAMC,YAAY;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,SAASC,iBACdC,iBAA6C,EAC7CC,MAAmB;IAEnB,MAAMC,SAAsB,aAAKD;IAEjC,uEAAuE;IACvE,8EAA8E;IAC9E,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,4EAA4E;IAC5E,8BAA8B;IAC9B,IAAIH,aAAaI,QAAQA,MAAM,CAACJ,UAAU,GAAG;IAE7C,KAAK,MAAMK,cAAcN,sBAAsBG,mBAAoB;QACjE,IAAIG,WAAWC,IAAI,KAAK,eAAe;YACrCF,MAAM,CAACC,WAAWE,IAAI,CAAC,GAAGC,WACxBJ,MAAM,CAACC,WAAWE,IAAI,CAAC,EACvBF,WAAWI,YAAY;YAEzB;QACF;QACA,IAAIJ,WAAWC,IAAI,KAAK,QAAQ,CAACD,WAAWI,YAAY,EAAE;QAC1D,IAAI,CAAEJ,CAAAA,WAAWE,IAAI,IAAIH,MAAK,GAAI;QAClCA,MAAM,CAACC,WAAWE,IAAI,CAAC,GAAG;IAC5B;IAEA,OAAOH;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,SAASI,WACPE,KAAmC,EACnCC,KAAc;IAEd,IAAIA,OAAO,OAAO,EAAE;IACpB,OAAOd,cAAca,OAAOE,GAAG,CAACC,CAAAA,MAAQ,aAAKA;YAAK,CAACf,QAAQ,EAAEF;;AAC/D"}
1
+ {"version":3,"sources":["../../../src/entity-definition/reconstruct/clone-entity-draft.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport { EntityDraft, type EntityDraftValue } from '../../types/EntityDraft.js';\nimport {\n newRowKey,\n readRowDrafts,\n ROW_KEY,\n} from '../../types/EntityRowDraft.js';\nimport { describeEntityColumns } from '../describe/index.js';\n\n/** The member `Entity` requires every record to carry. */\nconst ID_MEMBER = 'id';\n\n/**\n * The draft for a copy of a record: everything the original held, minus the\n * members a copy must not carry.\n *\n * **A draft in and a draft out**, rather than an instance in and an instance\n * out, and that is the whole reason this composes with the rest of the form\n * layer. A form's state is a `Record<string, string>` that a workspace\n * autosaves, so a clone that returned an instance would have to be\n * re-seeded back into a draft by the caller — through `seedFieldValue`, which\n * lives in a package above this one. Staying in the draft keeps the operation\n * on one side of that boundary, and `reconstructEntity` is still what turns the\n * result into an entity at submit.\n *\n * Two kinds of member are dropped:\n *\n * - **the identity member**, always, with no flag — a copy that carries the\n * original's `id` is an update to the original, which is the one outcome a\n * Clone button must never produce;\n * - **anything declared `@accessor({ resetOnClone: true })`** — a unique code,\n * an audit stamp, a sequence number.\n *\n * Cleared to `''` rather than deleted, because `''` is what an empty control\n * drafts as: a member removed from the record entirely would arrive\n * `undefined`, and an input handed `undefined` flips from controlled to\n * uncontrolled — the same trap `restoreEntityDraft` layers a restored draft\n * over a seeded one to avoid.\n *\n * A member the entity declares but the draft never held stays absent, so this\n * neither invents keys nor widens the draft.\n */\nexport function cloneEntityDraft<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n): EntityDraft {\n const cloned: EntityDraft = { ...values };\n\n // The identity member, cleared **without consulting the descriptors**.\n // `describeEntityColumns` drops `hidden` members, and a form hiding its id is\n // the ordinary case rather than the exotic one — every generated catalog page\n // does it — so a descriptor-driven sweep would leave the id in place on\n // exactly the forms a Clone button appears on, and the \"copy\" would silently\n // save over the original. `Entity` fixes this member's name, so there is no\n // metadata to consult anyway.\n if (ID_MEMBER in cloned) cloned[ID_MEMBER] = '';\n\n for (const descriptor of describeEntityColumns(entityConstructor)) {\n if (descriptor.type === 'composition') {\n cloned[descriptor.name] = clonedRows(\n cloned[descriptor.name],\n descriptor.resetOnClone,\n );\n continue;\n }\n // An `id` member is reset unless it is the parent: a copy of a node is its\n // sibling, and a copy that came out as a root would be moved, not copied.\n const isIdentityLike = descriptor.type === 'id' && !descriptor.parent;\n if (!isIdentityLike && !descriptor.resetOnClone) continue;\n if (!(descriptor.name in cloned)) continue;\n cloned[descriptor.name] = '';\n }\n\n return cloned;\n}\n\n/**\n * The rows a copy starts with.\n *\n * Two things the `''` rule above cannot express. A collection is **emptied to\n * `[]`, never to `''`**: an empty string is not a readable row list, so\n * `reconstructEntity` would skip the member and the \"reset\" collection would\n * come back holding the original's lines — a `resetOnClone` that resets nothing.\n *\n * And a row that *is* copied gets a **fresh key**. The key identifies a row\n * within one editing session; carrying the original's over means the copy and\n * the original name their lines identically, so a workspace holding both open\n * has two tabs whose rows claim the same identity — harmless until anything\n * addresses a row across them, and impossible to debug once it does.\n */\nfunction clonedRows(\n value: EntityDraftValue | undefined,\n reset: boolean,\n): EntityDraftValue {\n if (reset) return [];\n return readRowDrafts(value).map(row => ({ ...row, [ROW_KEY]: newRowKey() }));\n}\n"],"names":["newRowKey","readRowDrafts","ROW_KEY","describeEntityColumns","ID_MEMBER","cloneEntityDraft","entityConstructor","values","cloned","descriptor","type","name","clonedRows","resetOnClone","isIdentityLike","parent","value","reset","map","row"],"mappings":";AAEA,SACEA,SAAS,EACTC,aAAa,EACbC,OAAO,QACF,gCAAgC;AACvC,SAASC,qBAAqB,QAAQ,uBAAuB;AAE7D,wDAAwD,GACxD,MAAMC,YAAY;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,SAASC,iBACdC,iBAA6C,EAC7CC,MAAmB;IAEnB,MAAMC,SAAsB,aAAKD;IAEjC,uEAAuE;IACvE,8EAA8E;IAC9E,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,4EAA4E;IAC5E,8BAA8B;IAC9B,IAAIH,aAAaI,QAAQA,MAAM,CAACJ,UAAU,GAAG;IAE7C,KAAK,MAAMK,cAAcN,sBAAsBG,mBAAoB;QACjE,IAAIG,WAAWC,IAAI,KAAK,eAAe;YACrCF,MAAM,CAACC,WAAWE,IAAI,CAAC,GAAGC,WACxBJ,MAAM,CAACC,WAAWE,IAAI,CAAC,EACvBF,WAAWI,YAAY;YAEzB;QACF;QACA,2EAA2E;QAC3E,0EAA0E;QAC1E,MAAMC,iBAAiBL,WAAWC,IAAI,KAAK,QAAQ,CAACD,WAAWM,MAAM;QACrE,IAAI,CAACD,kBAAkB,CAACL,WAAWI,YAAY,EAAE;QACjD,IAAI,CAAEJ,CAAAA,WAAWE,IAAI,IAAIH,MAAK,GAAI;QAClCA,MAAM,CAACC,WAAWE,IAAI,CAAC,GAAG;IAC5B;IAEA,OAAOH;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,SAASI,WACPI,KAAmC,EACnCC,KAAc;IAEd,IAAIA,OAAO,OAAO,EAAE;IACpB,OAAOhB,cAAce,OAAOE,GAAG,CAACC,CAAAA,MAAQ,aAAKA;YAAK,CAACjB,QAAQ,EAAEF;;AAC/D"}
@@ -55,7 +55,7 @@ import { reconstructChild } from './reconstruct-child.js';
55
55
  * `composition` is excluded for the reason `linkCollection` is: it has no
56
56
  * editor yet, so a draft never holds its rows and writing one would mean
57
57
  * writing `undefined` over the master's own lines on every save. The rows'
58
- * write path lands with the detail control (#122). `scalarCollection` is *not*
58
+ * write path lands with the detail control. `scalarCollection` is *not*
59
59
  * excluded — it is a scalar as far as this walk is concerned, because its
60
60
  * comma-list draft coerces losslessly back to a `string[]`.
61
61
  */ function isWritableScalar(descriptor, current) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport {\n EntityDraft,\n readDraftString,\n readLocalizedDraft,\n} from '../../types/EntityDraft.js';\nimport { isRowDraftArray } from '../../types/EntityRowDraft.js';\nimport {\n describeEntityColumns,\n EntityFieldDescriptor,\n} from '../describe/index.js';\nimport {\n applyEntityLinks,\n type EntityLinkSelection,\n} from '../links/apply-entity-links/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport {\n coerceFieldValue,\n coerceLocalizedDraft,\n} from './coerce-field-value.js';\nimport { reconstructChild } from './reconstruct-child.js';\n\n/** Everything {@link reconstructEntity} needs beyond the draft itself. */\nexport interface ReconstructEntityOptions<TEntity extends Entity> {\n /**\n * The record being edited. Supplies the `id` and nothing else — see\n * {@link reconstructEntity} on why it is only the id.\n */\n existing?: TEntity;\n /**\n * The instances a picker handed over, for members whose declared\n * `linkSerialization` is `embedded`. Passed straight to `applyEntityLinks`.\n */\n selection?: EntityLinkSelection;\n}\n\n/**\n * Writes an entity's owned collections back from the rows its draft holds.\n *\n * A **second pass** rather than a branch inside the scalar walk, and the reason\n * is the invariant it protects: `isWritableScalar` must go on refusing\n * `composition`, because assigning a coerced string to a member that holds rows\n * is how the master's own lines get blanked on a save that touches nothing else.\n * Keeping the two walks separate means a `composition` is written by exactly one\n * piece of code, and that code cannot be reached with a string.\n *\n * A member whose draft entry is missing or unreadable is **skipped, not\n * cleared** — the same rule the scalar walk applies to a member the form hides.\n * A draft written before this member existed, or one restored from a build that\n * did not know it, must leave the record's rows exactly as they were rather than\n * emptying them, which is a data loss the user never asked for and cannot see.\n * An explicitly empty list *is* a value and is written: that is a user who\n * removed every row.\n *\n * Read-only is honoured for the same reason as everywhere else, and the\n * `childType` guard is not defensive — a `composition` that declared no child\n * has no columns to coerce against, so there is nothing to rebuild.\n */\nfunction applyEntityComposition(\n instance: object,\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n): void {\n for (const descriptor of descriptors) {\n if (descriptor.type !== 'composition') continue;\n if (descriptor.readonly || descriptor.childType === undefined) continue;\n\n const drafted = values[descriptor.name];\n if (!isRowDraftArray(drafted)) continue;\n\n const rows = drafted;\n const childType = descriptor.childType;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = rows.map(row =>\n reconstructChild(childType, row),\n );\n }\n}\n\n/**\n * A member this function must not write.\n *\n * `readonly` is the declared half — `describeEntityColumns` keeps read-only\n * members because a form still *shows* them, so unlike the column list this\n * walk has to filter them itself, exactly as `buildEntityInstance` does.\n *\n * The relation half is checked twice on purpose. The declared `link` /\n * `linkCollection` types are `applyEntityLinks`' business, and the runtime\n * `instanceof` catches a bare `@accessor()` on a relation that declared no\n * type: those accessors are getter-only, so assigning a coerced string to one\n * would throw rather than misbehave quietly.\n *\n * `composition` is excluded for the reason `linkCollection` is: it has no\n * editor yet, so a draft never holds its rows and writing one would mean\n * writing `undefined` over the master's own lines on every save. The rows'\n * write path lands with the detail control (#122). `scalarCollection` is *not*\n * excluded — it is a scalar as far as this walk is concerned, because its\n * comma-list draft coerces losslessly back to a `string[]`.\n */\nfunction isWritableScalar(\n descriptor: EntityFieldDescriptor,\n current: unknown,\n): boolean {\n if (descriptor.readonly) return false;\n if (\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition'\n ) {\n return false;\n }\n return !(\n current instanceof EntityLink || current instanceof EntityCollectionLink\n );\n}\n\n/**\n * Rebuilds a domain instance from a form's string draft, deriving every\n * assignment from the entity's own accessor metadata.\n *\n * This is the one genuinely hand-written part of an entity form, and writing it\n * per entity is a bug farm: each one repeats the same empty-string-to-undefined\n * rule, and each one has to remember to carry back a member the form hides —\n * `ProductBrand.code` is assigned by the create transaction, and dropping it\n * blanks the record's identifier on every update. Deriving the walk from the\n * metadata makes that structural instead of remembered.\n *\n * **Construction is zero-argument.** `new entityConstructor()` then assignment\n * through setters, the same shape `buildEntityInstance` uses to rebuild a record\n * off the wire. Entities default their constructor parameters, and a required\n * member is filled by its setter a moment later — so required-ness stays one\n * fact (the `@accessor({ required })` flag the form already validates against)\n * rather than a second, unverifiable list of constructor argument names.\n *\n * Constructing first also gives `describeEntityColumns` a sample, which is what\n * lets it infer `link`-ness for an accessor that declared no `type`: relations\n * are initialized by the constructor, so the fresh instance already carries them.\n *\n * **`existing` supplies the id and nothing else.** Every other member comes from\n * the draft, which was seeded from the record — so a create/update distinction\n * lives here once instead of a `target.id = entity?.id` line per form. A draft\n * may be client-supplied (a workspace autosaves it), which is not a new surface:\n * a save route already overwrites the id from its own route parameter.\n *\n * Pure and framework-free, so the same reconstruction runs in a browser form and\n * in a backend use-case assembling an entity from a raw draft.\n */\nexport function reconstructEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n { existing, selection }: ReconstructEntityOptions<TEntity> = {},\n): TEntity {\n const instance = new entityConstructor();\n const descriptors = describeEntityColumns(entityConstructor, instance);\n\n for (const descriptor of descriptors) {\n // The draft is keyed by accessor name, and so is the assignment: `key` is\n // `alias ?? name`, which is the wire/SQL column, not the JS property.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const current = (instance as any)[descriptor.name];\n if (descriptor.type === 'localized') {\n if (descriptor.readonly) continue;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceLocalizedDraft(\n readLocalizedDraft(values, descriptor.name),\n );\n continue;\n }\n if (!isWritableScalar(descriptor, current)) continue;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceFieldValue(\n descriptor,\n readDraftString(values, descriptor.name),\n );\n }\n\n applyEntityComposition(instance, descriptors, values);\n applyEntityLinks(instance, descriptors, values, selection);\n\n if (existing !== undefined) instance.id = existing.id;\n\n return instance;\n}\n"],"names":["readDraftString","readLocalizedDraft","isRowDraftArray","describeEntityColumns","applyEntityLinks","EntityCollectionLink","EntityLink","coerceFieldValue","coerceLocalizedDraft","reconstructChild","applyEntityComposition","instance","descriptors","values","descriptor","type","readonly","childType","undefined","drafted","name","rows","map","row","isWritableScalar","current","reconstructEntity","entityConstructor","existing","selection","id"],"mappings":"AACA,SAEEA,eAAe,EACfC,kBAAkB,QACb,6BAA6B;AACpC,SAASC,eAAe,QAAQ,gCAAgC;AAChE,SACEC,qBAAqB,QAEhB,uBAAuB;AAC9B,SACEC,gBAAgB,QAEX,uCAAuC;AAC9C,SAASC,oBAAoB,QAAQ,2CAA2C;AAChF,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SACEC,gBAAgB,EAChBC,oBAAoB,QACf,0BAA0B;AACjC,SAASC,gBAAgB,QAAQ,yBAAyB;AAgB1D;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASC,uBACPC,QAAgB,EAChBC,WAA6C,EAC7CC,MAAmB;IAEnB,KAAK,MAAMC,cAAcF,YAAa;QACpC,IAAIE,WAAWC,IAAI,KAAK,eAAe;QACvC,IAAID,WAAWE,QAAQ,IAAIF,WAAWG,SAAS,KAAKC,WAAW;QAE/D,MAAMC,UAAUN,MAAM,CAACC,WAAWM,IAAI,CAAC;QACvC,IAAI,CAAClB,gBAAgBiB,UAAU;QAE/B,MAAME,OAAOF;QACb,MAAMF,YAAYH,WAAWG,SAAS;QAEtC,8DAA8D;QAC7DN,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGC,KAAKC,GAAG,CAACC,CAAAA,MAC5Cd,iBAAiBQ,WAAWM;IAEhC;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,iBACPV,UAAiC,EACjCW,OAAgB;IAEhB,IAAIX,WAAWE,QAAQ,EAAE,OAAO;IAChC,IACEF,WAAWC,IAAI,KAAK,UACpBD,WAAWC,IAAI,KAAK,oBACpBD,WAAWC,IAAI,KAAK,eACpB;QACA,OAAO;IACT;IACA,OAAO,CACLU,CAAAA,mBAAmBnB,cAAcmB,mBAAmBpB,oBAAmB;AAE3E;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASqB,kBACdC,iBAA6C,EAC7Cd,MAAmB,EACnB,EAAEe,QAAQ,EAAEC,SAAS,EAAqC,GAAG,CAAC,CAAC;IAE/D,MAAMlB,WAAW,IAAIgB;IACrB,MAAMf,cAAcT,sBAAsBwB,mBAAmBhB;IAE7D,KAAK,MAAMG,cAAcF,YAAa;QACpC,0EAA0E;QAC1E,sEAAsE;QACtE,8DAA8D;QAC9D,MAAMa,UAAU,AAACd,QAAgB,CAACG,WAAWM,IAAI,CAAC;QAClD,IAAIN,WAAWC,IAAI,KAAK,aAAa;YACnC,IAAID,WAAWE,QAAQ,EAAE;YACzB,8DAA8D;YAC7DL,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGZ,qBACnCP,mBAAmBY,QAAQC,WAAWM,IAAI;YAE5C;QACF;QACA,IAAI,CAACI,iBAAiBV,YAAYW,UAAU;QAE5C,8DAA8D;QAC7Dd,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGb,iBACnCO,YACAd,gBAAgBa,QAAQC,WAAWM,IAAI;IAE3C;IAEAV,uBAAuBC,UAAUC,aAAaC;IAC9CT,iBAAiBO,UAAUC,aAAaC,QAAQgB;IAEhD,IAAID,aAAaV,WAAWP,SAASmB,EAAE,GAAGF,SAASE,EAAE;IAErD,OAAOnB;AACT"}
1
+ {"version":3,"sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport {\n EntityDraft,\n readDraftString,\n readLocalizedDraft,\n} from '../../types/EntityDraft.js';\nimport { isRowDraftArray } from '../../types/EntityRowDraft.js';\nimport {\n describeEntityColumns,\n EntityFieldDescriptor,\n} from '../describe/index.js';\nimport {\n applyEntityLinks,\n type EntityLinkSelection,\n} from '../links/apply-entity-links/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport {\n coerceFieldValue,\n coerceLocalizedDraft,\n} from './coerce-field-value.js';\nimport { reconstructChild } from './reconstruct-child.js';\n\n/** Everything {@link reconstructEntity} needs beyond the draft itself. */\nexport interface ReconstructEntityOptions<TEntity extends Entity> {\n /**\n * The record being edited. Supplies the `id` and nothing else — see\n * {@link reconstructEntity} on why it is only the id.\n */\n existing?: TEntity;\n /**\n * The instances a picker handed over, for members whose declared\n * `linkSerialization` is `embedded`. Passed straight to `applyEntityLinks`.\n */\n selection?: EntityLinkSelection;\n}\n\n/**\n * Writes an entity's owned collections back from the rows its draft holds.\n *\n * A **second pass** rather than a branch inside the scalar walk, and the reason\n * is the invariant it protects: `isWritableScalar` must go on refusing\n * `composition`, because assigning a coerced string to a member that holds rows\n * is how the master's own lines get blanked on a save that touches nothing else.\n * Keeping the two walks separate means a `composition` is written by exactly one\n * piece of code, and that code cannot be reached with a string.\n *\n * A member whose draft entry is missing or unreadable is **skipped, not\n * cleared** — the same rule the scalar walk applies to a member the form hides.\n * A draft written before this member existed, or one restored from a build that\n * did not know it, must leave the record's rows exactly as they were rather than\n * emptying them, which is a data loss the user never asked for and cannot see.\n * An explicitly empty list *is* a value and is written: that is a user who\n * removed every row.\n *\n * Read-only is honoured for the same reason as everywhere else, and the\n * `childType` guard is not defensive — a `composition` that declared no child\n * has no columns to coerce against, so there is nothing to rebuild.\n */\nfunction applyEntityComposition(\n instance: object,\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n): void {\n for (const descriptor of descriptors) {\n if (descriptor.type !== 'composition') continue;\n if (descriptor.readonly || descriptor.childType === undefined) continue;\n\n const drafted = values[descriptor.name];\n if (!isRowDraftArray(drafted)) continue;\n\n const rows = drafted;\n const childType = descriptor.childType;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = rows.map(row =>\n reconstructChild(childType, row),\n );\n }\n}\n\n/**\n * A member this function must not write.\n *\n * `readonly` is the declared half — `describeEntityColumns` keeps read-only\n * members because a form still *shows* them, so unlike the column list this\n * walk has to filter them itself, exactly as `buildEntityInstance` does.\n *\n * The relation half is checked twice on purpose. The declared `link` /\n * `linkCollection` types are `applyEntityLinks`' business, and the runtime\n * `instanceof` catches a bare `@accessor()` on a relation that declared no\n * type: those accessors are getter-only, so assigning a coerced string to one\n * would throw rather than misbehave quietly.\n *\n * `composition` is excluded for the reason `linkCollection` is: it has no\n * editor yet, so a draft never holds its rows and writing one would mean\n * writing `undefined` over the master's own lines on every save. The rows'\n * write path lands with the detail control. `scalarCollection` is *not*\n * excluded — it is a scalar as far as this walk is concerned, because its\n * comma-list draft coerces losslessly back to a `string[]`.\n */\nfunction isWritableScalar(\n descriptor: EntityFieldDescriptor,\n current: unknown,\n): boolean {\n if (descriptor.readonly) return false;\n if (\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition'\n ) {\n return false;\n }\n return !(\n current instanceof EntityLink || current instanceof EntityCollectionLink\n );\n}\n\n/**\n * Rebuilds a domain instance from a form's string draft, deriving every\n * assignment from the entity's own accessor metadata.\n *\n * This is the one genuinely hand-written part of an entity form, and writing it\n * per entity is a bug farm: each one repeats the same empty-string-to-undefined\n * rule, and each one has to remember to carry back a member the form hides —\n * `ProductBrand.code` is assigned by the create transaction, and dropping it\n * blanks the record's identifier on every update. Deriving the walk from the\n * metadata makes that structural instead of remembered.\n *\n * **Construction is zero-argument.** `new entityConstructor()` then assignment\n * through setters, the same shape `buildEntityInstance` uses to rebuild a record\n * off the wire. Entities default their constructor parameters, and a required\n * member is filled by its setter a moment later — so required-ness stays one\n * fact (the `@accessor({ required })` flag the form already validates against)\n * rather than a second, unverifiable list of constructor argument names.\n *\n * Constructing first also gives `describeEntityColumns` a sample, which is what\n * lets it infer `link`-ness for an accessor that declared no `type`: relations\n * are initialized by the constructor, so the fresh instance already carries them.\n *\n * **`existing` supplies the id and nothing else.** Every other member comes from\n * the draft, which was seeded from the record — so a create/update distinction\n * lives here once instead of a `target.id = entity?.id` line per form. A draft\n * may be client-supplied (a workspace autosaves it), which is not a new surface:\n * a save route already overwrites the id from its own route parameter.\n *\n * Pure and framework-free, so the same reconstruction runs in a browser form and\n * in a backend use-case assembling an entity from a raw draft.\n */\nexport function reconstructEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n { existing, selection }: ReconstructEntityOptions<TEntity> = {},\n): TEntity {\n const instance = new entityConstructor();\n const descriptors = describeEntityColumns(entityConstructor, instance);\n\n for (const descriptor of descriptors) {\n // The draft is keyed by accessor name, and so is the assignment: `key` is\n // `alias ?? name`, which is the wire/SQL column, not the JS property.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const current = (instance as any)[descriptor.name];\n if (descriptor.type === 'localized') {\n if (descriptor.readonly) continue;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceLocalizedDraft(\n readLocalizedDraft(values, descriptor.name),\n );\n continue;\n }\n if (!isWritableScalar(descriptor, current)) continue;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceFieldValue(\n descriptor,\n readDraftString(values, descriptor.name),\n );\n }\n\n applyEntityComposition(instance, descriptors, values);\n applyEntityLinks(instance, descriptors, values, selection);\n\n if (existing !== undefined) instance.id = existing.id;\n\n return instance;\n}\n"],"names":["readDraftString","readLocalizedDraft","isRowDraftArray","describeEntityColumns","applyEntityLinks","EntityCollectionLink","EntityLink","coerceFieldValue","coerceLocalizedDraft","reconstructChild","applyEntityComposition","instance","descriptors","values","descriptor","type","readonly","childType","undefined","drafted","name","rows","map","row","isWritableScalar","current","reconstructEntity","entityConstructor","existing","selection","id"],"mappings":"AACA,SAEEA,eAAe,EACfC,kBAAkB,QACb,6BAA6B;AACpC,SAASC,eAAe,QAAQ,gCAAgC;AAChE,SACEC,qBAAqB,QAEhB,uBAAuB;AAC9B,SACEC,gBAAgB,QAEX,uCAAuC;AAC9C,SAASC,oBAAoB,QAAQ,2CAA2C;AAChF,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SACEC,gBAAgB,EAChBC,oBAAoB,QACf,0BAA0B;AACjC,SAASC,gBAAgB,QAAQ,yBAAyB;AAgB1D;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASC,uBACPC,QAAgB,EAChBC,WAA6C,EAC7CC,MAAmB;IAEnB,KAAK,MAAMC,cAAcF,YAAa;QACpC,IAAIE,WAAWC,IAAI,KAAK,eAAe;QACvC,IAAID,WAAWE,QAAQ,IAAIF,WAAWG,SAAS,KAAKC,WAAW;QAE/D,MAAMC,UAAUN,MAAM,CAACC,WAAWM,IAAI,CAAC;QACvC,IAAI,CAAClB,gBAAgBiB,UAAU;QAE/B,MAAME,OAAOF;QACb,MAAMF,YAAYH,WAAWG,SAAS;QAEtC,8DAA8D;QAC7DN,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGC,KAAKC,GAAG,CAACC,CAAAA,MAC5Cd,iBAAiBQ,WAAWM;IAEhC;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,iBACPV,UAAiC,EACjCW,OAAgB;IAEhB,IAAIX,WAAWE,QAAQ,EAAE,OAAO;IAChC,IACEF,WAAWC,IAAI,KAAK,UACpBD,WAAWC,IAAI,KAAK,oBACpBD,WAAWC,IAAI,KAAK,eACpB;QACA,OAAO;IACT;IACA,OAAO,CACLU,CAAAA,mBAAmBnB,cAAcmB,mBAAmBpB,oBAAmB;AAE3E;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASqB,kBACdC,iBAA6C,EAC7Cd,MAAmB,EACnB,EAAEe,QAAQ,EAAEC,SAAS,EAAqC,GAAG,CAAC,CAAC;IAE/D,MAAMlB,WAAW,IAAIgB;IACrB,MAAMf,cAAcT,sBAAsBwB,mBAAmBhB;IAE7D,KAAK,MAAMG,cAAcF,YAAa;QACpC,0EAA0E;QAC1E,sEAAsE;QACtE,8DAA8D;QAC9D,MAAMa,UAAU,AAACd,QAAgB,CAACG,WAAWM,IAAI,CAAC;QAClD,IAAIN,WAAWC,IAAI,KAAK,aAAa;YACnC,IAAID,WAAWE,QAAQ,EAAE;YACzB,8DAA8D;YAC7DL,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGZ,qBACnCP,mBAAmBY,QAAQC,WAAWM,IAAI;YAE5C;QACF;QACA,IAAI,CAACI,iBAAiBV,YAAYW,UAAU;QAE5C,8DAA8D;QAC7Dd,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGb,iBACnCO,YACAd,gBAAgBa,QAAQC,WAAWM,IAAI;IAE3C;IAEAV,uBAAuBC,UAAUC,aAAaC;IAC9CT,iBAAiBO,UAAUC,aAAaC,QAAQgB;IAEhD,IAAID,aAAaV,WAAWP,SAASmB,EAAE,GAAGF,SAASE,EAAE;IAErD,OAAOnB;AACT"}
@@ -18,15 +18,13 @@ import type { EntityLoadRequest } from '../types/EntityLoadRequest.js';
18
18
  * carry four shapes.** `transactionEvent` used to mean all of them, and a
19
19
  * discriminant that means several things asserts nothing when it is read —
20
20
  * which is what `readTransactionEventEnvelope` was doing on the `202`: checking
21
- * a name that the record route and the event type both also answered to
22
- * ([ADR 0055](../../../../../../docs/adr/0055-a-coordinator-resumes-from-its-own-record.md),
23
- * #176).
21
+ * a name that the record route and the event type both also answered to.
24
22
  *
25
23
  * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.
26
24
  * Not a record: it has no `createdAt` and names no entity id yet.
27
25
  * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.
28
26
  * - `sagaResult` — how a settled multi-step walk ended: which steps ran and
29
- * what each returned ([ADR 0052](../../../../../../docs/adr/0052-the-checkout-saga.md)).
27
+ * what each returned.
30
28
  * - `sagaInstance` — the durable instance itself, which is what a resumable
31
29
  * coordinator reads and what `GET /api/saga/:id` serves. It differs from
32
30
  * `sagaResult` in carrying the inputs and the resume count, because those are
@@ -81,7 +79,7 @@ export interface EntifixEventMeta {
81
79
  * makes a transaction's `completed` look like a duplicate of its `accepted`.
82
80
  */
83
81
  id: string;
84
- /** The emitting **slice** (ADR 0020's ownership noun), e.g. `marketplace-admin`. */
82
+ /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */
85
83
  source: string;
86
84
  /** ISO-8601 emission time. */
87
85
  at: string;
@@ -109,7 +107,7 @@ export interface EntifixEnvelopeMeta {
109
107
  *
110
108
  * Generic in the payload so a publisher keeps its own shape —
111
109
  * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when
112
- * ADR 0009's projection event lands — without the transport learning either.
110
+ * a projection's event lands — without the transport learning either.
113
111
  */
114
112
  export interface DomainEvent<TData = unknown> extends EntifixEventMeta {
115
113
  data: TData;
@@ -117,7 +115,7 @@ export interface DomainEvent<TData = unknown> extends EntifixEventMeta {
117
115
  /**
118
116
  * A change to an entity that happened on the server, delivered out-of-band —
119
117
  * the `data` of a bus/stream `event` envelope rather than a framing of its own
120
- * (ADR 0036). `entity` is the `key ?? name` an adapter routes on, the same
118
+ *. `entity` is the `key ?? name` an adapter routes on, the same
121
119
  * string `entityQueryScope` uses, so a subscriber can invalidate exactly the
122
120
  * affected query keys.
123
121
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,kBAAkB,GAClB,YAAY,GACZ,SAAS,GACT,OAAO,GACP,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,cAAc,GACd,gBAAgB,CAAC;AAErB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9B,qCAAqC;IACrC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,OAAO,CAAE,SAAQ,gBAAgB;IACpE,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1C,EAAE,EAAE,QAAQ,CAAC;CACd;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,KAAK,GAAG,OAAO;IAC9C,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACnE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,eAAe,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAC3E,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,IAC5D,eAAe,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;AACjD,MAAM,MAAM,sBAAsB,GAAG,eAAe,CAAC,sBAAsB,CAAC,CAAC;AAE7E;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,GAAG,OAAO,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG;IACpE,IAAI,EAAE,mBAAmB,GAAG;QAAE,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAC;CACzD,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,kBAAkB,GAClB,YAAY,GACZ,SAAS,GACT,OAAO,GACP,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,cAAc,GACd,gBAAgB,CAAC;AAErB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9B,qCAAqC;IACrC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,OAAO,CAAE,SAAQ,gBAAgB;IACpE,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1C,EAAE,EAAE,QAAQ,CAAC;CACd;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,KAAK,GAAG,OAAO;IAC9C,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACnE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC;AAC/D,MAAM,MAAM,wBAAwB,GAAG,eAAe,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAC3E,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,IAC5D,eAAe,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;AACjD,MAAM,MAAM,sBAAsB,GAAG,eAAe,CAAC,sBAAsB,CAAC,CAAC;AAE7E;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,GAAG,OAAO,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG;IACpE,IAAI,EAAE,mBAAmB,GAAG;QAAE,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAC;CACzD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to\n * ([ADR 0055](../../../../../../docs/adr/0055-a-coordinator-resumes-from-its-own-record.md),\n * #176).\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned ([ADR 0052](../../../../../../docs/adr/0052-the-checkout-saga.md)).\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** (ADR 0020's ownership noun), e.g. `marketplace-admin`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * ADR 0009's projection event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n * (ADR 0036). `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"AAyLA;;;CAGC,GACD,WAEE"}
1
+ {"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to.\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned.\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * a projection's event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n *. `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"AAuLA;;;CAGC,GACD,WAEE"}
@@ -0,0 +1,77 @@
1
+ import type { ChildConstructor } from '../entity-definition/meta-entities/meta-accessor/index.js';
2
+ /**
3
+ * How to read a record's place in a tree: its own key, and its parent's key or
4
+ * `undefined` for a root. Every helper here takes one, so the same walk serves
5
+ * an entity, a plain fixture row and a draft.
6
+ */
7
+ export interface HierarchyKeys<T> {
8
+ readonly idOf: (item: T) => string;
9
+ readonly parentOf: (item: T) => string | undefined;
10
+ }
11
+ /** One record and the records whose parent it is, in their incoming order. */
12
+ export interface TreeNode<T> {
13
+ readonly item: T;
14
+ readonly depth: number;
15
+ readonly children: readonly TreeNode<T>[];
16
+ }
17
+ /**
18
+ * A set of records arranged as a tree.
19
+ *
20
+ * `detached` holds the records on a cycle — `a` is `b`'s parent and `b` is
21
+ * `a`'s. No root leads to them, so they are in no subtree. They are
22
+ * returned rather than dropped, because a record that silently disappears
23
+ * from a listing is a defect nobody can see. A host that enforces the
24
+ * invariant at save ({@link wouldCycle}) never has any.
25
+ */
26
+ export interface Tree<T> {
27
+ readonly roots: readonly TreeNode<T>[];
28
+ readonly detached: readonly T[];
29
+ }
30
+ /** A tree node as one row of a flat listing. */
31
+ export interface TreeRow<T> {
32
+ readonly item: T;
33
+ readonly depth: number;
34
+ readonly hasChildren: boolean;
35
+ /** The keys of every ancestor, root first. */
36
+ readonly path: readonly string[];
37
+ }
38
+ /**
39
+ * Arrange records as a tree.
40
+ *
41
+ * A record whose parent is not in the set is a **root**: a page of records
42
+ * holds children whose parents are on another page, and a retired or deleted
43
+ * parent leaves its children behind. Siblings keep the order they came in,
44
+ * so a sorted load stays sorted within each level.
45
+ */
46
+ export declare function buildTree<T>(items: readonly T[], keys: HierarchyKeys<T>): Tree<T>;
47
+ /** The tree as rows, depth first: each record followed by its subtree. */
48
+ export declare function flattenTree<T>(tree: Tree<T>, keys: HierarchyKeys<T>): TreeRow<T>[];
49
+ /**
50
+ * The ancestors of `key`, root first, not including the record itself.
51
+ *
52
+ * Stops at the first key it has already visited, so data that holds a cycle
53
+ * yields a finite chain rather than a hung render.
54
+ */
55
+ export declare function ancestorsOf<T>(items: readonly T[], keys: HierarchyKeys<T>, key: string): T[];
56
+ /** The keys of every record under `key`, at any depth. Cycle-safe. */
57
+ export declare function descendantsOf<T>(items: readonly T[], keys: HierarchyKeys<T>, key: string): string[];
58
+ /**
59
+ * Would giving `key` the parent `parent` make a cycle? True when the parent is
60
+ * the record itself or anything under it. The check a save runs, and the set a
61
+ * parent picker excludes.
62
+ */
63
+ export declare function wouldCycle<T>(items: readonly T[], keys: HierarchyKeys<T>, key: string, parent: string | undefined): boolean;
64
+ /** How deep `key` sits: 1 for a root, 2 for its children, and so on. */
65
+ export declare function depthOf<T>(items: readonly T[], keys: HierarchyKeys<T>, key: string): number;
66
+ /**
67
+ * The keys an entity's own metadata declares: its `id`, and the member marked
68
+ * `@accessor({ parent: true })` — read by its wire key, the way a record
69
+ * arrives. `undefined` when the entity declares no parent and so is not a
70
+ * tree.
71
+ *
72
+ * Refuses a `hidden` or `readonly` parent, for the reason describing an
73
+ * entity does: such a member is never serialized, so the tree would arrive
74
+ * flat.
75
+ */
76
+ export declare function hierarchyOf<T extends object>(target: ChildConstructor<T>): HierarchyKeys<T> | undefined;
77
+ //# sourceMappingURL=hierarchy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hierarchy.d.ts","sourceRoot":"","sources":["../../src/hierarchy/hierarchy.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAElG;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;CACpD;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;CAC3C;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC;CACjC;AAED,gDAAgD;AAChD,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAQD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GACrB,IAAI,CAAC,CAAC,CAAC,CA+BT;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACb,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,CAAC,CAAC,EAAE,CAed;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,GAAG,EAAE,MAAM,GACV,CAAC,EAAE,CAaL;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,CAAC,EAC7B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAqBV;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAGT;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,CAAC,EACvB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,GAAG,EAAE,MAAM,GACV,MAAM,CAER;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC1C,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,CA2B9B"}
@@ -0,0 +1,165 @@
1
+ import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
2
+ import { assertOneParent, assertParentVisible } from '../entity-definition/describe/index.js';
3
+ import { extractMetaAccessors } from '../entity-definition/helpers/index.js';
4
+ /** An empty parent key is no parent, the way an empty form control is no value. */ const parentKey = (keys, item)=>{
5
+ const parent = keys.parentOf(item);
6
+ return parent === undefined || parent === '' ? undefined : parent;
7
+ };
8
+ /**
9
+ * Arrange records as a tree.
10
+ *
11
+ * A record whose parent is not in the set is a **root**: a page of records
12
+ * holds children whose parents are on another page, and a retired or deleted
13
+ * parent leaves its children behind. Siblings keep the order they came in,
14
+ * so a sorted load stays sorted within each level.
15
+ */ export function buildTree(items, keys) {
16
+ const byKey = new Map(items.map((item)=>[
17
+ keys.idOf(item),
18
+ item
19
+ ]));
20
+ const childrenOf = new Map();
21
+ const roots = [];
22
+ for (const item of items){
23
+ const parent = parentKey(keys, item);
24
+ if (parent === undefined || !byKey.has(parent)) {
25
+ roots.push(item);
26
+ } else {
27
+ var _childrenOf_get;
28
+ childrenOf.set(parent, [
29
+ ...(_childrenOf_get = childrenOf.get(parent)) != null ? _childrenOf_get : [],
30
+ item
31
+ ]);
32
+ }
33
+ }
34
+ const placed = new Set();
35
+ const node = (item, depth)=>{
36
+ var _childrenOf_get;
37
+ const key = keys.idOf(item);
38
+ placed.add(key);
39
+ return {
40
+ item,
41
+ depth,
42
+ children: ((_childrenOf_get = childrenOf.get(key)) != null ? _childrenOf_get : []).map((child)=>node(child, depth + 1))
43
+ };
44
+ };
45
+ const tree = roots.map((root)=>node(root, 0));
46
+ return {
47
+ roots: tree,
48
+ detached: items.filter((item)=>!placed.has(keys.idOf(item)))
49
+ };
50
+ }
51
+ /** The tree as rows, depth first: each record followed by its subtree. */ export function flattenTree(tree, keys) {
52
+ const rows = [];
53
+ const walk = (node, path)=>{
54
+ rows.push({
55
+ item: node.item,
56
+ depth: node.depth,
57
+ hasChildren: node.children.length > 0,
58
+ path
59
+ });
60
+ for (const child of node.children){
61
+ walk(child, [
62
+ ...path,
63
+ keys.idOf(node.item)
64
+ ]);
65
+ }
66
+ };
67
+ for (const root of tree.roots)walk(root, []);
68
+ return rows;
69
+ }
70
+ /**
71
+ * The ancestors of `key`, root first, not including the record itself.
72
+ *
73
+ * Stops at the first key it has already visited, so data that holds a cycle
74
+ * yields a finite chain rather than a hung render.
75
+ */ export function ancestorsOf(items, keys, key) {
76
+ const byKey = new Map(items.map((item)=>[
77
+ keys.idOf(item),
78
+ item
79
+ ]));
80
+ const chain = [];
81
+ const seen = new Set([
82
+ key
83
+ ]);
84
+ let current = byKey.get(key);
85
+ while(current !== undefined){
86
+ const parent = parentKey(keys, current);
87
+ if (parent === undefined || seen.has(parent)) break;
88
+ seen.add(parent);
89
+ current = byKey.get(parent);
90
+ if (current !== undefined) chain.unshift(current);
91
+ }
92
+ return chain;
93
+ }
94
+ /** The keys of every record under `key`, at any depth. Cycle-safe. */ export function descendantsOf(items, keys, key) {
95
+ var _childrenOf_get;
96
+ const childrenOf = new Map();
97
+ for (const item of items){
98
+ var _childrenOf_get1;
99
+ const parent = parentKey(keys, item);
100
+ if (parent === undefined) continue;
101
+ childrenOf.set(parent, [
102
+ ...(_childrenOf_get1 = childrenOf.get(parent)) != null ? _childrenOf_get1 : [],
103
+ keys.idOf(item)
104
+ ]);
105
+ }
106
+ const found = [];
107
+ const seen = new Set([
108
+ key
109
+ ]);
110
+ const queue = [
111
+ ...(_childrenOf_get = childrenOf.get(key)) != null ? _childrenOf_get : []
112
+ ];
113
+ while(queue.length > 0){
114
+ var _childrenOf_get2;
115
+ const next = queue.shift();
116
+ if (seen.has(next)) continue;
117
+ seen.add(next);
118
+ found.push(next);
119
+ queue.push(...(_childrenOf_get2 = childrenOf.get(next)) != null ? _childrenOf_get2 : []);
120
+ }
121
+ return found;
122
+ }
123
+ /**
124
+ * Would giving `key` the parent `parent` make a cycle? True when the parent is
125
+ * the record itself or anything under it. The check a save runs, and the set a
126
+ * parent picker excludes.
127
+ */ export function wouldCycle(items, keys, key, parent) {
128
+ if (parent === undefined || parent === '') return false;
129
+ return parent === key || descendantsOf(items, keys, key).includes(parent);
130
+ }
131
+ /** How deep `key` sits: 1 for a root, 2 for its children, and so on. */ export function depthOf(items, keys, key) {
132
+ return ancestorsOf(items, keys, key).length + 1;
133
+ }
134
+ /**
135
+ * The keys an entity's own metadata declares: its `id`, and the member marked
136
+ * `@accessor({ parent: true })` — read by its wire key, the way a record
137
+ * arrives. `undefined` when the entity declares no parent and so is not a
138
+ * tree.
139
+ *
140
+ * Refuses a `hidden` or `readonly` parent, for the reason describing an
141
+ * entity does: such a member is never serialized, so the tree would arrive
142
+ * flat.
143
+ */ export function hierarchyOf(target) {
144
+ var _parent_alias;
145
+ const accessors = extractMetaAccessors(target).filter((accessor)=>accessor.kind === 'getter');
146
+ assertOneParent(accessors);
147
+ assertParentVisible(accessors);
148
+ const parent = accessors.find((accessor)=>accessor.parent === true);
149
+ if (parent === undefined) return undefined;
150
+ if (parent.type !== undefined && parent.type !== 'id' && parent.type !== 'string') {
151
+ throw new EntifixBuildError(`"${String(parent.name)}" is a ${parent.type} and cannot be the parent member — a parent is one record's id, so it is an id or a string.`);
152
+ }
153
+ const member = String((_parent_alias = parent.alias) != null ? _parent_alias : parent.name);
154
+ const read = (item, name)=>item[name];
155
+ const asKey = (value)=>value === undefined || value === null ? undefined : String(value);
156
+ return {
157
+ idOf: (item)=>{
158
+ var _read;
159
+ return String((_read = read(item, 'id')) != null ? _read : '');
160
+ },
161
+ parentOf: (item)=>asKey(read(item, member))
162
+ };
163
+ }
164
+
165
+ //# sourceMappingURL=hierarchy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hierarchy/hierarchy.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport {\n assertOneParent,\n assertParentVisible,\n} from '../entity-definition/describe/index.js';\nimport { extractMetaAccessors } from '../entity-definition/helpers/index.js';\nimport type { ChildConstructor } from '../entity-definition/meta-entities/meta-accessor/index.js';\n\n/**\n * How to read a record's place in a tree: its own key, and its parent's key or\n * `undefined` for a root. Every helper here takes one, so the same walk serves\n * an entity, a plain fixture row and a draft.\n */\nexport interface HierarchyKeys<T> {\n readonly idOf: (item: T) => string;\n readonly parentOf: (item: T) => string | undefined;\n}\n\n/** One record and the records whose parent it is, in their incoming order. */\nexport interface TreeNode<T> {\n readonly item: T;\n readonly depth: number;\n readonly children: readonly TreeNode<T>[];\n}\n\n/**\n * A set of records arranged as a tree.\n *\n * `detached` holds the records on a cycle — `a` is `b`'s parent and `b` is\n * `a`'s. No root leads to them, so they are in no subtree. They are\n * returned rather than dropped, because a record that silently disappears\n * from a listing is a defect nobody can see. A host that enforces the\n * invariant at save ({@link wouldCycle}) never has any.\n */\nexport interface Tree<T> {\n readonly roots: readonly TreeNode<T>[];\n readonly detached: readonly T[];\n}\n\n/** A tree node as one row of a flat listing. */\nexport interface TreeRow<T> {\n readonly item: T;\n readonly depth: number;\n readonly hasChildren: boolean;\n /** The keys of every ancestor, root first. */\n readonly path: readonly string[];\n}\n\n/** An empty parent key is no parent, the way an empty form control is no value. */\nconst parentKey = <T>(keys: HierarchyKeys<T>, item: T) => {\n const parent = keys.parentOf(item);\n return parent === undefined || parent === '' ? undefined : parent;\n};\n\n/**\n * Arrange records as a tree.\n *\n * A record whose parent is not in the set is a **root**: a page of records\n * holds children whose parents are on another page, and a retired or deleted\n * parent leaves its children behind. Siblings keep the order they came in,\n * so a sorted load stays sorted within each level.\n */\nexport function buildTree<T>(\n items: readonly T[],\n keys: HierarchyKeys<T>,\n): Tree<T> {\n const byKey = new Map(items.map(item => [keys.idOf(item), item]));\n const childrenOf = new Map<string, T[]>();\n const roots: T[] = [];\n for (const item of items) {\n const parent = parentKey(keys, item);\n if (parent === undefined || !byKey.has(parent)) {\n roots.push(item);\n } else {\n childrenOf.set(parent, [...(childrenOf.get(parent) ?? []), item]);\n }\n }\n\n const placed = new Set<string>();\n const node = (item: T, depth: number): TreeNode<T> => {\n const key = keys.idOf(item);\n placed.add(key);\n return {\n item,\n depth,\n children: (childrenOf.get(key) ?? []).map(child =>\n node(child, depth + 1),\n ),\n };\n };\n const tree = roots.map(root => node(root, 0));\n\n return {\n roots: tree,\n detached: items.filter(item => !placed.has(keys.idOf(item))),\n };\n}\n\n/** The tree as rows, depth first: each record followed by its subtree. */\nexport function flattenTree<T>(\n tree: Tree<T>,\n keys: HierarchyKeys<T>,\n): TreeRow<T>[] {\n const rows: TreeRow<T>[] = [];\n const walk = (node: TreeNode<T>, path: readonly string[]) => {\n rows.push({\n item: node.item,\n depth: node.depth,\n hasChildren: node.children.length > 0,\n path,\n });\n for (const child of node.children) {\n walk(child, [...path, keys.idOf(node.item)]);\n }\n };\n for (const root of tree.roots) walk(root, []);\n return rows;\n}\n\n/**\n * The ancestors of `key`, root first, not including the record itself.\n *\n * Stops at the first key it has already visited, so data that holds a cycle\n * yields a finite chain rather than a hung render.\n */\nexport function ancestorsOf<T>(\n items: readonly T[],\n keys: HierarchyKeys<T>,\n key: string,\n): T[] {\n const byKey = new Map(items.map(item => [keys.idOf(item), item]));\n const chain: T[] = [];\n const seen = new Set([key]);\n let current = byKey.get(key);\n while (current !== undefined) {\n const parent = parentKey(keys, current);\n if (parent === undefined || seen.has(parent)) break;\n seen.add(parent);\n current = byKey.get(parent);\n if (current !== undefined) chain.unshift(current);\n }\n return chain;\n}\n\n/** The keys of every record under `key`, at any depth. Cycle-safe. */\nexport function descendantsOf<T>(\n items: readonly T[],\n keys: HierarchyKeys<T>,\n key: string,\n): string[] {\n const childrenOf = new Map<string, string[]>();\n for (const item of items) {\n const parent = parentKey(keys, item);\n if (parent === undefined) continue;\n childrenOf.set(parent, [\n ...(childrenOf.get(parent) ?? []),\n keys.idOf(item),\n ]);\n }\n const found: string[] = [];\n const seen = new Set([key]);\n const queue = [...(childrenOf.get(key) ?? [])];\n while (queue.length > 0) {\n const next = queue.shift() as string;\n if (seen.has(next)) continue;\n seen.add(next);\n found.push(next);\n queue.push(...(childrenOf.get(next) ?? []));\n }\n return found;\n}\n\n/**\n * Would giving `key` the parent `parent` make a cycle? True when the parent is\n * the record itself or anything under it. The check a save runs, and the set a\n * parent picker excludes.\n */\nexport function wouldCycle<T>(\n items: readonly T[],\n keys: HierarchyKeys<T>,\n key: string,\n parent: string | undefined,\n): boolean {\n if (parent === undefined || parent === '') return false;\n return parent === key || descendantsOf(items, keys, key).includes(parent);\n}\n\n/** How deep `key` sits: 1 for a root, 2 for its children, and so on. */\nexport function depthOf<T>(\n items: readonly T[],\n keys: HierarchyKeys<T>,\n key: string,\n): number {\n return ancestorsOf(items, keys, key).length + 1;\n}\n\n/**\n * The keys an entity's own metadata declares: its `id`, and the member marked\n * `@accessor({ parent: true })` — read by its wire key, the way a record\n * arrives. `undefined` when the entity declares no parent and so is not a\n * tree.\n *\n * Refuses a `hidden` or `readonly` parent, for the reason describing an\n * entity does: such a member is never serialized, so the tree would arrive\n * flat.\n */\nexport function hierarchyOf<T extends object>(\n target: ChildConstructor<T>,\n): HierarchyKeys<T> | undefined {\n const accessors = extractMetaAccessors(target).filter(\n accessor => accessor.kind === 'getter',\n );\n assertOneParent(accessors);\n assertParentVisible(accessors);\n const parent = accessors.find(accessor => accessor.parent === true);\n if (parent === undefined) return undefined;\n if (\n parent.type !== undefined &&\n parent.type !== 'id' &&\n parent.type !== 'string'\n ) {\n throw new EntifixBuildError(\n `\"${String(parent.name)}\" is a ${parent.type} and cannot be the parent member — a parent is one record's id, so it is an id or a string.`,\n );\n }\n const member = String(parent.alias ?? parent.name);\n const read = (item: T, name: string) =>\n (item as Record<string, unknown>)[name];\n const asKey = (value: unknown) =>\n value === undefined || value === null ? undefined : String(value);\n\n return {\n idOf: item => String(read(item, 'id') ?? ''),\n parentOf: item => asKey(read(item, member)),\n };\n}\n"],"names":["EntifixBuildError","assertOneParent","assertParentVisible","extractMetaAccessors","parentKey","keys","item","parent","parentOf","undefined","buildTree","items","byKey","Map","map","idOf","childrenOf","roots","has","push","set","get","placed","Set","node","depth","key","add","children","child","tree","root","detached","filter","flattenTree","rows","walk","path","hasChildren","length","ancestorsOf","chain","seen","current","unshift","descendantsOf","found","queue","next","shift","wouldCycle","includes","depthOf","hierarchyOf","target","accessors","accessor","kind","find","type","String","name","member","alias","read","asKey","value"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAC5E,SACEC,eAAe,EACfC,mBAAmB,QACd,yCAAyC;AAChD,SAASC,oBAAoB,QAAQ,wCAAwC;AA2C7E,iFAAiF,GACjF,MAAMC,YAAY,CAAIC,MAAwBC;IAC5C,MAAMC,SAASF,KAAKG,QAAQ,CAACF;IAC7B,OAAOC,WAAWE,aAAaF,WAAW,KAAKE,YAAYF;AAC7D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASG,UACdC,KAAmB,EACnBN,IAAsB;IAEtB,MAAMO,QAAQ,IAAIC,IAAIF,MAAMG,GAAG,CAACR,CAAAA,OAAQ;YAACD,KAAKU,IAAI,CAACT;YAAOA;SAAK;IAC/D,MAAMU,aAAa,IAAIH;IACvB,MAAMI,QAAa,EAAE;IACrB,KAAK,MAAMX,QAAQK,MAAO;QACxB,MAAMJ,SAASH,UAAUC,MAAMC;QAC/B,IAAIC,WAAWE,aAAa,CAACG,MAAMM,GAAG,CAACX,SAAS;YAC9CU,MAAME,IAAI,CAACb;QACb,OAAO;gBACuBU;YAA5BA,WAAWI,GAAG,CAACb,QAAQ;oBAAKS,kBAAAA,WAAWK,GAAG,CAACd,mBAAfS,kBAA0B,EAAE;gBAAGV;aAAK;QAClE;IACF;IAEA,MAAMgB,SAAS,IAAIC;IACnB,MAAMC,OAAO,CAAClB,MAASmB;YAMRT;QALb,MAAMU,MAAMrB,KAAKU,IAAI,CAACT;QACtBgB,OAAOK,GAAG,CAACD;QACX,OAAO;YACLpB;YACAmB;YACAG,UAAU,EAACZ,kBAAAA,WAAWK,GAAG,CAACK,gBAAfV,kBAAuB,EAAE,EAAEF,GAAG,CAACe,CAAAA,QACxCL,KAAKK,OAAOJ,QAAQ;QAExB;IACF;IACA,MAAMK,OAAOb,MAAMH,GAAG,CAACiB,CAAAA,OAAQP,KAAKO,MAAM;IAE1C,OAAO;QACLd,OAAOa;QACPE,UAAUrB,MAAMsB,MAAM,CAAC3B,CAAAA,OAAQ,CAACgB,OAAOJ,GAAG,CAACb,KAAKU,IAAI,CAACT;IACvD;AACF;AAEA,wEAAwE,GACxE,OAAO,SAAS4B,YACdJ,IAAa,EACbzB,IAAsB;IAEtB,MAAM8B,OAAqB,EAAE;IAC7B,MAAMC,OAAO,CAACZ,MAAmBa;QAC/BF,KAAKhB,IAAI,CAAC;YACRb,MAAMkB,KAAKlB,IAAI;YACfmB,OAAOD,KAAKC,KAAK;YACjBa,aAAad,KAAKI,QAAQ,CAACW,MAAM,GAAG;YACpCF;QACF;QACA,KAAK,MAAMR,SAASL,KAAKI,QAAQ,CAAE;YACjCQ,KAAKP,OAAO;mBAAIQ;gBAAMhC,KAAKU,IAAI,CAACS,KAAKlB,IAAI;aAAE;QAC7C;IACF;IACA,KAAK,MAAMyB,QAAQD,KAAKb,KAAK,CAAEmB,KAAKL,MAAM,EAAE;IAC5C,OAAOI;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASK,YACd7B,KAAmB,EACnBN,IAAsB,EACtBqB,GAAW;IAEX,MAAMd,QAAQ,IAAIC,IAAIF,MAAMG,GAAG,CAACR,CAAAA,OAAQ;YAACD,KAAKU,IAAI,CAACT;YAAOA;SAAK;IAC/D,MAAMmC,QAAa,EAAE;IACrB,MAAMC,OAAO,IAAInB,IAAI;QAACG;KAAI;IAC1B,IAAIiB,UAAU/B,MAAMS,GAAG,CAACK;IACxB,MAAOiB,YAAYlC,UAAW;QAC5B,MAAMF,SAASH,UAAUC,MAAMsC;QAC/B,IAAIpC,WAAWE,aAAaiC,KAAKxB,GAAG,CAACX,SAAS;QAC9CmC,KAAKf,GAAG,CAACpB;QACToC,UAAU/B,MAAMS,GAAG,CAACd;QACpB,IAAIoC,YAAYlC,WAAWgC,MAAMG,OAAO,CAACD;IAC3C;IACA,OAAOF;AACT;AAEA,oEAAoE,GACpE,OAAO,SAASI,cACdlC,KAAmB,EACnBN,IAAsB,EACtBqB,GAAW;QAaQV;IAXnB,MAAMA,aAAa,IAAIH;IACvB,KAAK,MAAMP,QAAQK,MAAO;YAIlBK;QAHN,MAAMT,SAASH,UAAUC,MAAMC;QAC/B,IAAIC,WAAWE,WAAW;QAC1BO,WAAWI,GAAG,CAACb,QAAQ;gBACjBS,mBAAAA,WAAWK,GAAG,CAACd,mBAAfS,mBAA0B,EAAE;YAChCX,KAAKU,IAAI,CAACT;SACX;IACH;IACA,MAAMwC,QAAkB,EAAE;IAC1B,MAAMJ,OAAO,IAAInB,IAAI;QAACG;KAAI;IAC1B,MAAMqB,QAAQ;YAAK/B,kBAAAA,WAAWK,GAAG,CAACK,gBAAfV,kBAAuB,EAAE;KAAE;IAC9C,MAAO+B,MAAMR,MAAM,GAAG,EAAG;YAKRvB;QAJf,MAAMgC,OAAOD,MAAME,KAAK;QACxB,IAAIP,KAAKxB,GAAG,CAAC8B,OAAO;QACpBN,KAAKf,GAAG,CAACqB;QACTF,MAAM3B,IAAI,CAAC6B;QACXD,MAAM5B,IAAI,KAAKH,mBAAAA,WAAWK,GAAG,CAAC2B,iBAAfhC,mBAAwB,EAAE;IAC3C;IACA,OAAO8B;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASI,WACdvC,KAAmB,EACnBN,IAAsB,EACtBqB,GAAW,EACXnB,MAA0B;IAE1B,IAAIA,WAAWE,aAAaF,WAAW,IAAI,OAAO;IAClD,OAAOA,WAAWmB,OAAOmB,cAAclC,OAAON,MAAMqB,KAAKyB,QAAQ,CAAC5C;AACpE;AAEA,sEAAsE,GACtE,OAAO,SAAS6C,QACdzC,KAAmB,EACnBN,IAAsB,EACtBqB,GAAW;IAEX,OAAOc,YAAY7B,OAAON,MAAMqB,KAAKa,MAAM,GAAG;AAChD;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASc,YACdC,MAA2B;QAkBL/C;IAhBtB,MAAMgD,YAAYpD,qBAAqBmD,QAAQrB,MAAM,CACnDuB,CAAAA,WAAYA,SAASC,IAAI,KAAK;IAEhCxD,gBAAgBsD;IAChBrD,oBAAoBqD;IACpB,MAAMhD,SAASgD,UAAUG,IAAI,CAACF,CAAAA,WAAYA,SAASjD,MAAM,KAAK;IAC9D,IAAIA,WAAWE,WAAW,OAAOA;IACjC,IACEF,OAAOoD,IAAI,KAAKlD,aAChBF,OAAOoD,IAAI,KAAK,QAChBpD,OAAOoD,IAAI,KAAK,UAChB;QACA,MAAM,IAAI3D,kBACR,CAAC,CAAC,EAAE4D,OAAOrD,OAAOsD,IAAI,EAAE,OAAO,EAAEtD,OAAOoD,IAAI,CAAC,2FAA2F,CAAC;IAE7I;IACA,MAAMG,SAASF,QAAOrD,gBAAAA,OAAOwD,KAAK,YAAZxD,gBAAgBA,OAAOsD,IAAI;IACjD,MAAMG,OAAO,CAAC1D,MAASuD,OACrB,AAACvD,IAAgC,CAACuD,KAAK;IACzC,MAAMI,QAAQ,CAACC,QACbA,UAAUzD,aAAayD,UAAU,OAAOzD,YAAYmD,OAAOM;IAE7D,OAAO;QACLnD,MAAMT,CAAAA;gBAAe0D;mBAAPJ,QAAOI,QAAAA,KAAK1D,MAAM,iBAAX0D,QAAoB;;QACzCxD,UAAUF,CAAAA,OAAQ2D,MAAMD,KAAK1D,MAAMwD;IACrC;AACF"}