@entifix/react-integration 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +8 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -1
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js +95 -3
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -1
- package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -1
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js +8 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -1
- package/package.json +7 -9
|
@@ -84,6 +84,14 @@ export interface EntityDraftMessages {
|
|
|
84
84
|
number(field: string): string;
|
|
85
85
|
date(field: string): string;
|
|
86
86
|
option(field: string): string;
|
|
87
|
+
/** A collection holding fewer than its `minItems`. */
|
|
88
|
+
minItems(field: string, count: number): string;
|
|
89
|
+
/** A collection holding more than its `maxItems`. */
|
|
90
|
+
maxItems(field: string, count: number): string;
|
|
91
|
+
/** A date before the one its `notBefore` names. */
|
|
92
|
+
notBefore(field: string, other: string): string;
|
|
93
|
+
/** A localized member with no text for one of the declared locales. */
|
|
94
|
+
locale(field: string, locale: string): string;
|
|
87
95
|
}
|
|
88
96
|
/**
|
|
89
97
|
* Validates a draft against what the metadata implies — `required` members must
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,
|
|
1
|
+
{"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCollectionLink, EntityLink, isRowDraftArray, joinFieldPath, readDraftString, readRowDrafts, ROW_KEY, rowFieldPath, seededRowKey } from '@entifix/core';
|
|
2
|
+
import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCollectionLink, EntityLink, isLocalizedText, isRowDraftArray, joinFieldPath, locales, readDraftString, readLocalizedDraft, readRowDrafts, ROW_KEY, rowFieldPath, seededRowKey } from '@entifix/core';
|
|
3
3
|
/**
|
|
4
4
|
* A member no form writes back, whatever the draft says about it — and the only
|
|
5
5
|
* one is a read-only member, because it is the only one no editor may ever
|
|
@@ -54,8 +54,26 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
|
|
|
54
54
|
if (descriptor.type === 'composition') {
|
|
55
55
|
return seedRowDrafts(descriptor, entity);
|
|
56
56
|
}
|
|
57
|
+
if (descriptor.type === 'localized') {
|
|
58
|
+
return seedLocalizedDraft(descriptor, entity);
|
|
59
|
+
}
|
|
57
60
|
return seedScalarValue(descriptor, entity);
|
|
58
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A `localized` member seeds one input per declared locale, each holding that
|
|
64
|
+
* locale's text or `''`. Every declared locale is present even when the record
|
|
65
|
+
* lacks it, so the form shows the empty input a translator has to fill.
|
|
66
|
+
*/ function seedLocalizedDraft(descriptor, entity) {
|
|
67
|
+
const raw = entity == null ? void 0 : entity[descriptor.name];
|
|
68
|
+
const text = isLocalizedText(raw) ? raw : {};
|
|
69
|
+
return Object.fromEntries(locales().map((locale)=>{
|
|
70
|
+
var _text_locale;
|
|
71
|
+
return [
|
|
72
|
+
locale,
|
|
73
|
+
(_text_locale = text[locale]) != null ? _text_locale : ''
|
|
74
|
+
];
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
59
77
|
/** {@link seedFieldValue} for every member that round-trips through a string. */ function seedScalarValue(descriptor, entity) {
|
|
60
78
|
if (entity == null) return '';
|
|
61
79
|
const raw = entity[descriptor.name];
|
|
@@ -142,6 +160,14 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
|
|
|
142
160
|
if (isRowDraftArray(value)) restored[descriptor.name] = value;
|
|
143
161
|
continue;
|
|
144
162
|
}
|
|
163
|
+
if (descriptor.type === 'localized') {
|
|
164
|
+
// Over the seed rather than instead of it, so a locale declared since
|
|
165
|
+
// the draft was saved still gets its (empty) input.
|
|
166
|
+
if (isLocalizedText(value)) {
|
|
167
|
+
restored[descriptor.name] = _extends({}, readLocalizedDraft(seed, descriptor.name), readLocalizedDraft(persisted, descriptor.name));
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
145
171
|
if (typeof value === 'string') restored[descriptor.name] = value;
|
|
146
172
|
}
|
|
147
173
|
return restored;
|
|
@@ -174,22 +200,82 @@ const labelOf = (descriptor)=>descriptor.label;
|
|
|
174
200
|
*/ export function validateEntityDraft(descriptors, values, messages, labelFor = labelOf) {
|
|
175
201
|
const errors = {};
|
|
176
202
|
for (const descriptor of descriptors){
|
|
203
|
+
var _formatMessage;
|
|
177
204
|
if (isNeverEdited(descriptor)) continue;
|
|
178
205
|
if (descriptor.type === 'composition') {
|
|
179
206
|
Object.assign(errors, validateComposition(descriptor, values, messages, labelFor));
|
|
180
207
|
continue;
|
|
181
208
|
}
|
|
209
|
+
if (descriptor.type === 'localized') {
|
|
210
|
+
const message = localizedMessage(descriptor, values, messages, labelFor);
|
|
211
|
+
if (message !== undefined) errors[descriptor.name] = message;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
182
214
|
const raw = readDraftString(values, descriptor.name);
|
|
183
215
|
if (descriptor.required && raw.trim() === '') {
|
|
184
216
|
errors[descriptor.name] = messages.required(labelFor(descriptor));
|
|
185
217
|
continue;
|
|
186
218
|
}
|
|
219
|
+
// Before the guard below: an empty list is a count of zero, and a link
|
|
220
|
+
// collection's count is checkable though its ids are not.
|
|
221
|
+
const counted = isCountedList(descriptor) ? itemCountMessage(descriptor, countItems(raw), messages, labelFor) : undefined;
|
|
222
|
+
if (counted !== undefined) {
|
|
223
|
+
errors[descriptor.name] = counted;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
187
226
|
if (raw === '' || !hasCheckableFormat(descriptor)) continue;
|
|
188
|
-
const message = formatMessage(descriptor, raw, messages, labelFor);
|
|
227
|
+
const message = (_formatMessage = formatMessage(descriptor, raw, messages, labelFor)) != null ? _formatMessage : orderMessage(descriptor, raw, descriptors, values, messages, labelFor);
|
|
189
228
|
if (message !== undefined) errors[descriptor.name] = message;
|
|
190
229
|
}
|
|
191
230
|
return errors;
|
|
192
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* A localized member is all or nothing: left wholly empty it is absent, which
|
|
234
|
+
* only `required` objects to; once any locale holds text, every declared one
|
|
235
|
+
* must — the rule `validateRecords` holds stored content to, so a page never
|
|
236
|
+
* shows one language in the middle of another.
|
|
237
|
+
*/ function localizedMessage(descriptor, values, messages, labelFor) {
|
|
238
|
+
const draft = readLocalizedDraft(values, descriptor.name);
|
|
239
|
+
const missing = locales().filter((locale)=>{
|
|
240
|
+
var _draft_locale;
|
|
241
|
+
return ((_draft_locale = draft[locale]) != null ? _draft_locale : '').trim() === '';
|
|
242
|
+
});
|
|
243
|
+
if (missing.length === locales().length) {
|
|
244
|
+
return descriptor.required ? messages.required(labelFor(descriptor)) : undefined;
|
|
245
|
+
}
|
|
246
|
+
const [first] = missing;
|
|
247
|
+
return first === undefined ? undefined : messages.locale(labelFor(descriptor), first);
|
|
248
|
+
}
|
|
249
|
+
/** A collection whose draft is a comma list: values, or the ids of links. */ function isCountedList(descriptor) {
|
|
250
|
+
return descriptor.type === 'scalarCollection' || descriptor.type === 'linkCollection';
|
|
251
|
+
}
|
|
252
|
+
/** How many values a draft's comma list holds, blanks aside. */ function countItems(raw) {
|
|
253
|
+
return raw.split(',').filter((item)=>item.trim() !== '').length;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A collection's `minItems` / `maxItems`, judged on its count. The same rule
|
|
257
|
+
* `validateRecords` applies to a stored record, so a draft the form accepts is
|
|
258
|
+
* one a static build would too.
|
|
259
|
+
*/ function itemCountMessage(descriptor, count, messages, labelFor) {
|
|
260
|
+
const { minItems, maxItems } = descriptor;
|
|
261
|
+
if (minItems !== undefined && count < minItems) {
|
|
262
|
+
return messages.minItems(labelFor(descriptor), minItems);
|
|
263
|
+
}
|
|
264
|
+
if (maxItems !== undefined && count > maxItems) {
|
|
265
|
+
return messages.maxItems(labelFor(descriptor), maxItems);
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* `notBefore`: a filled date that precedes the filled date it names. Either one
|
|
271
|
+
* empty, or unparseable, is someone else's message.
|
|
272
|
+
*/ function orderMessage(descriptor, raw, descriptors, values, messages, labelFor) {
|
|
273
|
+
if (descriptor.notBefore === undefined) return undefined;
|
|
274
|
+
const earlier = descriptors.find((other)=>other.name === descriptor.notBefore);
|
|
275
|
+
const start = new Date(readDraftString(values, descriptor.notBefore));
|
|
276
|
+
if (earlier === undefined || Number.isNaN(start.getTime())) return undefined;
|
|
277
|
+
return new Date(raw) < start ? messages.notBefore(labelFor(descriptor), labelFor(earlier)) : undefined;
|
|
278
|
+
}
|
|
193
279
|
/**
|
|
194
280
|
* The metadata rule for one filled scalar draft value, or `undefined` when it
|
|
195
281
|
* passes.
|
|
@@ -229,6 +315,8 @@ const labelOf = (descriptor)=>descriptor.label;
|
|
|
229
315
|
errors[descriptor.name] = messages.required(labelFor(descriptor));
|
|
230
316
|
return errors;
|
|
231
317
|
}
|
|
318
|
+
const counted = itemCountMessage(descriptor, rows.length, messages, labelFor);
|
|
319
|
+
if (counted !== undefined) errors[descriptor.name] = counted;
|
|
232
320
|
if (descriptor.childType === undefined) return errors;
|
|
233
321
|
const columns = editableChildColumns(describeChildColumns(descriptor.childType));
|
|
234
322
|
rows.forEach((row, index)=>{
|
|
@@ -332,7 +420,11 @@ const labelOf = (descriptor)=>descriptor.label;
|
|
|
332
420
|
required: (field)=>`validation.required:${field}`,
|
|
333
421
|
number: (field)=>`validation.number:${field}`,
|
|
334
422
|
date: (field)=>`validation.date:${field}`,
|
|
335
|
-
option: (field)=>`validation.option:${field}
|
|
423
|
+
option: (field)=>`validation.option:${field}`,
|
|
424
|
+
minItems: (field, count)=>`validation.minItems:${field}:${count}`,
|
|
425
|
+
maxItems: (field, count)=>`validation.maxItems:${field}:${count}`,
|
|
426
|
+
notBefore: (field, other)=>`validation.notBefore:${field}:${other}`,
|
|
427
|
+
locale: (field, locale)=>`validation.locale:${field}:${locale}`
|
|
336
428
|
};
|
|
337
429
|
|
|
338
430
|
//# sourceMappingURL=use-entity-form.helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"sourcesContent":["import {\n describeChildColumns,\n editableChildColumns,\n EntifixLogicError,\n EntityCollectionLink,\n type EntityDraft,\n type EntityDraftValue,\n type EntityFieldDescriptor,\n EntityLink,\n type EntityRowDraft,\n isRowDraftArray,\n joinFieldPath,\n readDraftString,\n readRowDrafts,\n ROW_KEY,\n rowFieldPath,\n seededRowKey,\n type StandardSchemaV1,\n type StandardSchemaV1Issue,\n} from '@entifix/core';\n\n/**\n * A member no form writes back, whatever the draft says about it — and the only\n * one is a read-only member, because it is the only one no editor may ever\n * exist for.\n *\n * `linkCollection` used to be here too, which meant `required` on a to-many\n * relation was silently unenforced: a rule the entity declared and the form\n * never applied. Whether a collection has an editor yet is a question about the\n * controls, not about what the domain insists on, so it is excluded from the\n * *format* check below instead.\n */\nfunction isNeverEdited(descriptor: EntityFieldDescriptor): boolean {\n return descriptor.readonly;\n}\n\n/**\n * A member whose *format* the metadata can judge. Both relation shapes are\n * excluded: their draft value is a foreign key (or a joined list of them), and\n * \"is this a real id\" is a question only the service can answer — but whether\n * one is present at all is checked, which is what `required` on a relation has\n * to mean.\n *\n * A `composition` is excluded here too, but it is no longer unjudged: its rows\n * are checked one at a time by {@link validateRowDrafts}, against the child's\n * own descriptors. What has no meaning is a *format* rule on the collection\n * itself — there is no string to judge, only rows.\n *\n * `scalarCollection` is deliberately *not* excluded — its comma list is real\n * text the user typed.\n */\nfunction hasCheckableFormat(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type !== 'link' &&\n descriptor.type !== 'linkCollection' &&\n descriptor.type !== 'composition'\n );\n}\n\n/**\n * The value a field seeds with from a record.\n *\n * A scalar seeds as a **string**: links with their foreign key(s), dates with a\n * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.\n *\n * The array branch is explicit rather than left to `String(raw)`, even though\n * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by\n * way of `Array.prototype.toString`, which is an accident: it was doing the\n * work of a `scalarCollection` seed before the type existed, and\n * `reconstructEntity` was handing the same string straight back as a `string`.\n * Both halves were wrong in the same direction, so the fixed-point spec could\n * not see it. Now the join is declared here and the split is declared there,\n * and they are inverses on purpose.\n *\n * A **`composition`** is the one member that does not seed as a string, because\n * it has no lossless string form at all — its rows seed as row drafts, each\n * with a freshly minted key. That key is the only thing here the record does not\n * supply, and it is minted at seed time rather than at render so that the rows a\n * form starts with are already addressable.\n */\nexport function seedFieldValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityDraftValue {\n if (descriptor.type === 'composition') {\n return seedRowDrafts(descriptor, entity);\n }\n return seedScalarValue(descriptor, entity);\n}\n\n/** {@link seedFieldValue} for every member that round-trips through a string. */\nfunction seedScalarValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): string {\n if (entity == null) return '';\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (raw == null) return '';\n if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);\n if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');\n if (raw instanceof Date) return raw.toISOString().slice(0, 10);\n if (Array.isArray(raw)) return raw.map(String).join(',');\n return String(raw);\n}\n\n/**\n * The rows a composition member seeds with.\n *\n * Each row is walked with the child's **own** descriptors, through the same\n * `seedScalarValue` the master's members use — so a child's `date` seeds as\n * `yyyy-mm-dd` on a line exactly as it would on a record, and\n * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.\n *\n * A row arrives as a plain object as readily as an instance (a payload off the\n * wire, a fixture), which is fine: nothing here asks what class it is, matching\n * `ChildConstructor`'s rule that a child is never `instanceof`-tested.\n *\n * A member with no declared child seeds as no rows — there are no columns to\n * walk, so there is nothing a row could be.\n */\nfunction seedRowDrafts(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityRowDraft[] {\n if (entity == null || descriptor.childType === undefined) return [];\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (!Array.isArray(raw)) return [];\n\n const columns = describeChildColumns(descriptor.childType);\n\n return raw.map((row, index) => {\n // Deterministic, so re-seeding the same record produces the same draft —\n // see `seededRowKey` for the render loop the random one causes.\n const draft: EntityRowDraft = { [ROW_KEY]: seededRowKey(index) };\n for (const column of columns) {\n draft[column.name] = seedScalarValue(column, row);\n }\n return draft;\n });\n}\n\n/** Builds the initial draft for a record (or an empty one for a create). */\nexport function seedEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n entity: unknown,\n): EntityDraft {\n const draft: EntityDraft = {};\n for (const descriptor of descriptors) {\n draft[descriptor.name] = seedFieldValue(descriptor, entity);\n }\n return draft;\n}\n\n/**\n * Restores a persisted draft **over** a freshly seeded one, rather than in place\n * of it.\n *\n * A draft outlives the entity that wrote it — it sits in IndexedDB across\n * refreshes and deployments — so the two can disagree by the time it comes back.\n * Replacing the seed outright is how that disagreement becomes a bug the user\n * sees: a member added since the draft was written has no entry, reaches the form\n * engine as `undefined`, and its input flips from controlled to uncontrolled\n * mid-render. A member since removed goes the other way, surviving as a value\n * nothing renders and `reconstructEntity` cannot assign.\n *\n * So the seed decides the *keys* and the draft decides the *values*: a name the\n * entity no longer declares is dropped, and a name it declares that the draft\n * lacks keeps its seeded value.\n *\n * **A value is restored only in the shape its member can hold**, which is now\n * two shapes rather than one. A scalar takes a string; a `composition` takes a\n * readable row list. Anything else is dropped back to its seed — a member that\n * changed type between the write and the read, or a row list from a build that\n * did not carry keys, restores as the record's own value instead of as\n * something no editor can render. This is per member, so one unreadable entry\n * never costs the user the rest of the form.\n *\n * This is the shape guard at the one place the shape is actually known. The\n * store-level version check (`drafts-state.ts`) can only decide that a whole\n * generation of drafts is unreadable; it cannot know which members one entity\n * declares.\n */\nexport function restoreEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n seed: EntityDraft,\n persisted: EntityDraft | undefined,\n): EntityDraft {\n if (persisted === undefined) return seed;\n\n const restored: EntityDraft = { ...seed };\n for (const descriptor of descriptors) {\n const value = persisted[descriptor.name];\n if (value === undefined) continue;\n\n if (descriptor.type === 'composition') {\n if (isRowDraftArray(value)) restored[descriptor.name] = value;\n continue;\n }\n if (typeof value === 'string') restored[descriptor.name] = value;\n }\n return restored;\n}\n\n/** The slice of a form field's state this module reads: its error list. */\nexport interface FieldErrorMeta {\n errors?: readonly unknown[];\n}\n\n/**\n * Flattens the form engine's per-field state into the `name → message` map the\n * agnostic `EntityForm` renders. Only the first message per field survives: a\n * row shows one line, and the metadata rules already stop at the first failure.\n *\n * Kept structural rather than typed against the engine so the rest of this\n * module stays independent of which form library backs the hook.\n */\nexport function readFieldErrors(\n fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const [name, meta] of Object.entries(fieldMeta)) {\n const message = meta?.errors?.find(entry => typeof entry === 'string');\n if (typeof message === 'string') errors[name] = message;\n }\n\n return errors;\n}\n\n/**\n * The four metadata-derived messages, already localized. Taken as an argument\n * rather than built here so this stays a pure function — and because the\n * sentence cannot be assembled from a label plus a hardcoded suffix: word order\n * differs between locales.\n */\n/**\n * How a message names a field. The default is the descriptor's `label`, which is\n * the accessor's `label` option or its humanized member name — never translated.\n * A host with a catalog resolves `labelKey` instead, so the message names the\n * field in the words its input and column header already use.\n */\nexport type EntityFieldLabeler = (descriptor: EntityFieldDescriptor) => string;\n\nconst labelOf: EntityFieldLabeler = descriptor => descriptor.label;\n\nexport interface EntityDraftMessages {\n required(field: string): string;\n number(field: string): string;\n date(field: string): string;\n option(field: string): string;\n}\n\n/**\n * Validates a draft against what the metadata implies — `required` members must\n * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only\n * members are skipped entirely; a relation is checked for presence but never\n * for format, since its draft value is a foreign key this side cannot resolve.\n *\n * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}\n * is what layers a schema and the caller's own rules over this.\n */\nexport function validateEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler = labelOf,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const descriptor of descriptors) {\n if (isNeverEdited(descriptor)) continue;\n\n if (descriptor.type === 'composition') {\n Object.assign(\n errors,\n validateComposition(descriptor, values, messages, labelFor),\n );\n continue;\n }\n\n const raw = readDraftString(values, descriptor.name);\n\n if (descriptor.required && raw.trim() === '') {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n continue;\n }\n if (raw === '' || !hasCheckableFormat(descriptor)) continue;\n\n const message = formatMessage(descriptor, raw, messages, labelFor);\n if (message !== undefined) errors[descriptor.name] = message;\n }\n\n return errors;\n}\n\n/**\n * The metadata rule for one filled scalar draft value, or `undefined` when it\n * passes.\n *\n * Extracted so a row's cell is judged by the **same** rule as a record's field.\n * Two copies is how a child's `number` member would quietly start accepting\n * something its master's would reject.\n */\nfunction formatMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {\n return messages.number(labelFor(descriptor));\n }\n if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {\n return messages.date(labelFor(descriptor));\n }\n if (\n descriptor.type === 'enum' &&\n descriptor.enumValues &&\n !descriptor.enumValues.includes(raw)\n ) {\n return messages.option(labelFor(descriptor));\n }\n return undefined;\n}\n\n/**\n * The rules an owned collection carries, at both of its levels.\n *\n * **`required` on the collection means \"at least one row\"** — the only thing a\n * master can assert about a collection it owns, and a genuinely different fact\n * from `required` on a child member, which is per row. Conflating the two would\n * make an order with three lines, one of them blank, indistinguishable from an\n * order with none.\n *\n * A row's own members are judged against the child's descriptors and reported at\n * `items[2].quantity`, so the grid can put the message in the cell that caused\n * it. Read-only child members are skipped for the reason they always are: no\n * editor exists for them, so no rule the user could satisfy applies.\n */\nfunction validateComposition(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n const rows = readRowDrafts(values[descriptor.name]);\n\n if (descriptor.required && rows.length === 0) {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n return errors;\n }\n if (descriptor.childType === undefined) return errors;\n\n const columns = editableChildColumns(\n describeChildColumns(descriptor.childType),\n );\n\n rows.forEach((row, index) => {\n for (const column of columns) {\n if (isNeverEdited(column)) continue;\n\n const raw = row[column.name] ?? '';\n const path = rowFieldPath(descriptor.name, index, column.name);\n\n if (column.required && raw.trim() === '') {\n errors[path] = messages.required(labelFor(column));\n continue;\n }\n if (raw === '') continue;\n\n const message = formatMessage(column, raw, messages, labelFor);\n if (message !== undefined) errors[path] = message;\n }\n });\n\n return errors;\n}\n\n/**\n * The error-map key an issue belongs to — the **whole** path, not its head.\n *\n * It used to read `issue.path?.[0]`, which was right while every member was a\n * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`\n * collapsed to `'items'`, so a rule about the third line's quantity reported\n * itself against the collection and the grid had no way to know which cell it\n * meant. Joining instead produces `items[2].quantity`, which is the key\n * `validateComposition` already writes — so a schema rule and a metadata rule\n * address the same cell the same way and `composeEntityFormErrors` can merge\n * them.\n *\n * A one-segment path still yields exactly the head, so no existing rule changes\n * meaning.\n */\nfunction issueFieldName(issue: StandardSchemaV1Issue): string | undefined {\n const path = issue.path;\n if (path == null || path.length === 0) return undefined;\n\n const segments = path.map(segment => {\n const raw =\n typeof segment === 'object' && segment !== null && 'key' in segment\n ? segment.key\n : segment;\n return typeof raw === 'number' ? raw : String(raw);\n });\n\n return joinFieldPath(segments);\n}\n\n/**\n * Runs a Standard Schema over the draft and sorts its issues into per-field\n * messages plus a form-level one (an issue with no path — a cross-field rule).\n *\n * **Schemas are authored against the string draft**, so they coerce\n * (`z.coerce.number()`) rather than expect typed values: the draft is what the\n * inputs round-trip, and the same schema then validates exactly what the user\n * sees. Only the first issue per field survives, matching how a row renders.\n *\n * Synchronous rules only. An async schema returns a Promise the form's\n * synchronous validation pass cannot wait on, so it is rejected loudly rather\n * than silently passing.\n */\nexport function readSchemaIssues(\n schema: StandardSchemaV1,\n values: EntityDraft,\n translateIssue: (message: string, field: string | undefined) => string,\n): { fields: Record<string, string>; form?: string } {\n const result = schema['~standard'].validate(values);\n\n if (result instanceof Promise) {\n throw new EntifixLogicError(\n 'useEntityForm cannot run an async schema: its validation pass is ' +\n 'synchronous. Move the asynchronous rule into the `validate` option.',\n );\n }\n if (!result.issues) return { fields: {} };\n\n const fields: Record<string, string> = {};\n let form: string | undefined;\n\n for (const issue of result.issues) {\n const name = issueFieldName(issue);\n const message = translateIssue(issue.message, name);\n\n if (name === undefined) form ??= message;\n else fields[name] ??= message;\n }\n\n return { fields, form };\n}\n\n/** Everything {@link composeEntityFormErrors} needs to judge one draft. */\nexport interface ComposeEntityFormErrorsOptions {\n descriptors: readonly EntityFieldDescriptor[];\n values: EntityDraft;\n messages: EntityDraftMessages;\n /** How a metadata message names its field. Defaults to the untranslated `label`. */\n labelFor?: EntityFieldLabeler;\n schema?: StandardSchemaV1;\n translateIssue: (message: string, field: string | undefined) => string;\n validate?: (values: EntityDraft) => Record<string, string>;\n}\n\n/**\n * The single composition point for a draft's errors: metadata rules first, then\n * the entity's schema, then the caller's `validate` callback. Later sources win\n * on conflict, so a caller can always override a message it disagrees with.\n */\nexport function composeEntityFormErrors({\n descriptors,\n values,\n messages,\n labelFor,\n schema,\n translateIssue,\n validate,\n}: ComposeEntityFormErrorsOptions): {\n fields: Record<string, string>;\n form?: string;\n} {\n const metadata = validateEntityDraft(descriptors, values, messages, labelFor);\n const schemaIssues = schema\n ? readSchemaIssues(schema, values, translateIssue)\n : { fields: {}, form: undefined };\n\n return {\n fields: { ...metadata, ...schemaIssues.fields, ...validate?.(values) },\n form: schemaIssues.form,\n };\n}\n\n/**\n * What a caller who supplies no {@link ValidationMessages} gets: the catalog key\n * itself, with the field appended.\n *\n * Honest rather than English — there is no catalog in this package to resolve\n * against — and visible enough in a form that a host notices it has wired no\n * copy. It sits beside the validator rather than in `.types.ts`, which this\n * repository keeps type-only.\n */\nexport const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages = {\n required: field => `validation.required:${field}`,\n number: field => `validation.number:${field}`,\n date: field => `validation.date:${field}`,\n option: field => `validation.option:${field}`,\n};\n"],"names":["describeChildColumns","editableChildColumns","EntifixLogicError","EntityCollectionLink","EntityLink","isRowDraftArray","joinFieldPath","readDraftString","readRowDrafts","ROW_KEY","rowFieldPath","seededRowKey","isNeverEdited","descriptor","readonly","hasCheckableFormat","type","seedFieldValue","entity","seedRowDrafts","seedScalarValue","raw","name","id","String","ids","map","join","Date","toISOString","slice","Array","isArray","childType","undefined","columns","row","index","draft","column","seedEntityDraft","descriptors","restoreEntityDraft","seed","persisted","restored","value","readFieldErrors","fieldMeta","errors","meta","Object","entries","message","find","entry","labelOf","label","validateEntityDraft","values","messages","labelFor","assign","validateComposition","required","trim","formatMessage","Number","isNaN","number","getTime","date","enumValues","includes","option","rows","length","forEach","path","issueFieldName","issue","segments","segment","key","readSchemaIssues","schema","translateIssue","result","validate","Promise","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"sourcesContent":["import {\n describeChildColumns,\n editableChildColumns,\n EntifixLogicError,\n EntityCollectionLink,\n type EntityDraft,\n type EntityDraftValue,\n type EntityFieldDescriptor,\n EntityLink,\n type EntityLocalizedDraft,\n type EntityRowDraft,\n isLocalizedText,\n isRowDraftArray,\n joinFieldPath,\n locales,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n ROW_KEY,\n rowFieldPath,\n seededRowKey,\n type StandardSchemaV1,\n type StandardSchemaV1Issue,\n} from '@entifix/core';\n\n/**\n * A member no form writes back, whatever the draft says about it — and the only\n * one is a read-only member, because it is the only one no editor may ever\n * exist for.\n *\n * `linkCollection` used to be here too, which meant `required` on a to-many\n * relation was silently unenforced: a rule the entity declared and the form\n * never applied. Whether a collection has an editor yet is a question about the\n * controls, not about what the domain insists on, so it is excluded from the\n * *format* check below instead.\n */\nfunction isNeverEdited(descriptor: EntityFieldDescriptor): boolean {\n return descriptor.readonly;\n}\n\n/**\n * A member whose *format* the metadata can judge. Both relation shapes are\n * excluded: their draft value is a foreign key (or a joined list of them), and\n * \"is this a real id\" is a question only the service can answer — but whether\n * one is present at all is checked, which is what `required` on a relation has\n * to mean.\n *\n * A `composition` is excluded here too, but it is no longer unjudged: its rows\n * are checked one at a time by {@link validateRowDrafts}, against the child's\n * own descriptors. What has no meaning is a *format* rule on the collection\n * itself — there is no string to judge, only rows.\n *\n * `scalarCollection` is deliberately *not* excluded — its comma list is real\n * text the user typed.\n */\nfunction hasCheckableFormat(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type !== 'link' &&\n descriptor.type !== 'linkCollection' &&\n descriptor.type !== 'composition'\n );\n}\n\n/**\n * The value a field seeds with from a record.\n *\n * A scalar seeds as a **string**: links with their foreign key(s), dates with a\n * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.\n *\n * The array branch is explicit rather than left to `String(raw)`, even though\n * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by\n * way of `Array.prototype.toString`, which is an accident: it was doing the\n * work of a `scalarCollection` seed before the type existed, and\n * `reconstructEntity` was handing the same string straight back as a `string`.\n * Both halves were wrong in the same direction, so the fixed-point spec could\n * not see it. Now the join is declared here and the split is declared there,\n * and they are inverses on purpose.\n *\n * A **`composition`** is the one member that does not seed as a string, because\n * it has no lossless string form at all — its rows seed as row drafts, each\n * with a freshly minted key. That key is the only thing here the record does not\n * supply, and it is minted at seed time rather than at render so that the rows a\n * form starts with are already addressable.\n */\nexport function seedFieldValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityDraftValue {\n if (descriptor.type === 'composition') {\n return seedRowDrafts(descriptor, entity);\n }\n if (descriptor.type === 'localized') {\n return seedLocalizedDraft(descriptor, entity);\n }\n return seedScalarValue(descriptor, entity);\n}\n\n/**\n * A `localized` member seeds one input per declared locale, each holding that\n * locale's text or `''`. Every declared locale is present even when the record\n * lacks it, so the form shows the empty input a translator has to fill.\n */\nfunction seedLocalizedDraft(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityLocalizedDraft {\n const raw = (entity as Record<string, unknown> | null | undefined)?.[\n descriptor.name\n ];\n const text = isLocalizedText(raw) ? raw : {};\n return Object.fromEntries(\n locales().map(locale => [locale, text[locale] ?? '']),\n );\n}\n\n/** {@link seedFieldValue} for every member that round-trips through a string. */\nfunction seedScalarValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): string {\n if (entity == null) return '';\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (raw == null) return '';\n if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);\n if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');\n if (raw instanceof Date) return raw.toISOString().slice(0, 10);\n if (Array.isArray(raw)) return raw.map(String).join(',');\n return String(raw);\n}\n\n/**\n * The rows a composition member seeds with.\n *\n * Each row is walked with the child's **own** descriptors, through the same\n * `seedScalarValue` the master's members use — so a child's `date` seeds as\n * `yyyy-mm-dd` on a line exactly as it would on a record, and\n * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.\n *\n * A row arrives as a plain object as readily as an instance (a payload off the\n * wire, a fixture), which is fine: nothing here asks what class it is, matching\n * `ChildConstructor`'s rule that a child is never `instanceof`-tested.\n *\n * A member with no declared child seeds as no rows — there are no columns to\n * walk, so there is nothing a row could be.\n */\nfunction seedRowDrafts(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityRowDraft[] {\n if (entity == null || descriptor.childType === undefined) return [];\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (!Array.isArray(raw)) return [];\n\n const columns = describeChildColumns(descriptor.childType);\n\n return raw.map((row, index) => {\n // Deterministic, so re-seeding the same record produces the same draft —\n // see `seededRowKey` for the render loop the random one causes.\n const draft: EntityRowDraft = { [ROW_KEY]: seededRowKey(index) };\n for (const column of columns) {\n draft[column.name] = seedScalarValue(column, row);\n }\n return draft;\n });\n}\n\n/** Builds the initial draft for a record (or an empty one for a create). */\nexport function seedEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n entity: unknown,\n): EntityDraft {\n const draft: EntityDraft = {};\n for (const descriptor of descriptors) {\n draft[descriptor.name] = seedFieldValue(descriptor, entity);\n }\n return draft;\n}\n\n/**\n * Restores a persisted draft **over** a freshly seeded one, rather than in place\n * of it.\n *\n * A draft outlives the entity that wrote it — it sits in IndexedDB across\n * refreshes and deployments — so the two can disagree by the time it comes back.\n * Replacing the seed outright is how that disagreement becomes a bug the user\n * sees: a member added since the draft was written has no entry, reaches the form\n * engine as `undefined`, and its input flips from controlled to uncontrolled\n * mid-render. A member since removed goes the other way, surviving as a value\n * nothing renders and `reconstructEntity` cannot assign.\n *\n * So the seed decides the *keys* and the draft decides the *values*: a name the\n * entity no longer declares is dropped, and a name it declares that the draft\n * lacks keeps its seeded value.\n *\n * **A value is restored only in the shape its member can hold**, which is now\n * two shapes rather than one. A scalar takes a string; a `composition` takes a\n * readable row list. Anything else is dropped back to its seed — a member that\n * changed type between the write and the read, or a row list from a build that\n * did not carry keys, restores as the record's own value instead of as\n * something no editor can render. This is per member, so one unreadable entry\n * never costs the user the rest of the form.\n *\n * This is the shape guard at the one place the shape is actually known. The\n * store-level version check (`drafts-state.ts`) can only decide that a whole\n * generation of drafts is unreadable; it cannot know which members one entity\n * declares.\n */\nexport function restoreEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n seed: EntityDraft,\n persisted: EntityDraft | undefined,\n): EntityDraft {\n if (persisted === undefined) return seed;\n\n const restored: EntityDraft = { ...seed };\n for (const descriptor of descriptors) {\n const value = persisted[descriptor.name];\n if (value === undefined) continue;\n\n if (descriptor.type === 'composition') {\n if (isRowDraftArray(value)) restored[descriptor.name] = value;\n continue;\n }\n if (descriptor.type === 'localized') {\n // Over the seed rather than instead of it, so a locale declared since\n // the draft was saved still gets its (empty) input.\n if (isLocalizedText(value)) {\n restored[descriptor.name] = {\n ...readLocalizedDraft(seed, descriptor.name),\n ...readLocalizedDraft(persisted, descriptor.name),\n };\n }\n continue;\n }\n if (typeof value === 'string') restored[descriptor.name] = value;\n }\n return restored;\n}\n\n/** The slice of a form field's state this module reads: its error list. */\nexport interface FieldErrorMeta {\n errors?: readonly unknown[];\n}\n\n/**\n * Flattens the form engine's per-field state into the `name → message` map the\n * agnostic `EntityForm` renders. Only the first message per field survives: a\n * row shows one line, and the metadata rules already stop at the first failure.\n *\n * Kept structural rather than typed against the engine so the rest of this\n * module stays independent of which form library backs the hook.\n */\nexport function readFieldErrors(\n fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const [name, meta] of Object.entries(fieldMeta)) {\n const message = meta?.errors?.find(entry => typeof entry === 'string');\n if (typeof message === 'string') errors[name] = message;\n }\n\n return errors;\n}\n\n/**\n * The four metadata-derived messages, already localized. Taken as an argument\n * rather than built here so this stays a pure function — and because the\n * sentence cannot be assembled from a label plus a hardcoded suffix: word order\n * differs between locales.\n */\n/**\n * How a message names a field. The default is the descriptor's `label`, which is\n * the accessor's `label` option or its humanized member name — never translated.\n * A host with a catalog resolves `labelKey` instead, so the message names the\n * field in the words its input and column header already use.\n */\nexport type EntityFieldLabeler = (descriptor: EntityFieldDescriptor) => string;\n\nconst labelOf: EntityFieldLabeler = descriptor => descriptor.label;\n\nexport interface EntityDraftMessages {\n required(field: string): string;\n number(field: string): string;\n date(field: string): string;\n option(field: string): string;\n /** A collection holding fewer than its `minItems`. */\n minItems(field: string, count: number): string;\n /** A collection holding more than its `maxItems`. */\n maxItems(field: string, count: number): string;\n /** A date before the one its `notBefore` names. */\n notBefore(field: string, other: string): string;\n /** A localized member with no text for one of the declared locales. */\n locale(field: string, locale: string): string;\n}\n\n/**\n * Validates a draft against what the metadata implies — `required` members must\n * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only\n * members are skipped entirely; a relation is checked for presence but never\n * for format, since its draft value is a foreign key this side cannot resolve.\n *\n * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}\n * is what layers a schema and the caller's own rules over this.\n */\nexport function validateEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler = labelOf,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const descriptor of descriptors) {\n if (isNeverEdited(descriptor)) continue;\n\n if (descriptor.type === 'composition') {\n Object.assign(\n errors,\n validateComposition(descriptor, values, messages, labelFor),\n );\n continue;\n }\n if (descriptor.type === 'localized') {\n const message = localizedMessage(descriptor, values, messages, labelFor);\n if (message !== undefined) errors[descriptor.name] = message;\n continue;\n }\n\n const raw = readDraftString(values, descriptor.name);\n\n if (descriptor.required && raw.trim() === '') {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n continue;\n }\n // Before the guard below: an empty list is a count of zero, and a link\n // collection's count is checkable though its ids are not.\n const counted = isCountedList(descriptor)\n ? itemCountMessage(descriptor, countItems(raw), messages, labelFor)\n : undefined;\n if (counted !== undefined) {\n errors[descriptor.name] = counted;\n continue;\n }\n if (raw === '' || !hasCheckableFormat(descriptor)) continue;\n\n const message =\n formatMessage(descriptor, raw, messages, labelFor) ??\n orderMessage(descriptor, raw, descriptors, values, messages, labelFor);\n if (message !== undefined) errors[descriptor.name] = message;\n }\n\n return errors;\n}\n\n/**\n * A localized member is all or nothing: left wholly empty it is absent, which\n * only `required` objects to; once any locale holds text, every declared one\n * must — the rule `validateRecords` holds stored content to, so a page never\n * shows one language in the middle of another.\n */\nfunction localizedMessage(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n const draft = readLocalizedDraft(values, descriptor.name);\n const missing = locales().filter(\n locale => (draft[locale] ?? '').trim() === '',\n );\n if (missing.length === locales().length) {\n return descriptor.required\n ? messages.required(labelFor(descriptor))\n : undefined;\n }\n const [first] = missing;\n return first === undefined\n ? undefined\n : messages.locale(labelFor(descriptor), first);\n}\n\n/** A collection whose draft is a comma list: values, or the ids of links. */\nfunction isCountedList(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type === 'scalarCollection' ||\n descriptor.type === 'linkCollection'\n );\n}\n\n/** How many values a draft's comma list holds, blanks aside. */\nfunction countItems(raw: string): number {\n return raw.split(',').filter(item => item.trim() !== '').length;\n}\n\n/**\n * A collection's `minItems` / `maxItems`, judged on its count. The same rule\n * `validateRecords` applies to a stored record, so a draft the form accepts is\n * one a static build would too.\n */\nfunction itemCountMessage(\n descriptor: EntityFieldDescriptor,\n count: number,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n const { minItems, maxItems } = descriptor;\n if (minItems !== undefined && count < minItems) {\n return messages.minItems(labelFor(descriptor), minItems);\n }\n if (maxItems !== undefined && count > maxItems) {\n return messages.maxItems(labelFor(descriptor), maxItems);\n }\n return undefined;\n}\n\n/**\n * `notBefore`: a filled date that precedes the filled date it names. Either one\n * empty, or unparseable, is someone else's message.\n */\nfunction orderMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n if (descriptor.notBefore === undefined) return undefined;\n const earlier = descriptors.find(\n other => other.name === descriptor.notBefore,\n );\n const start = new Date(readDraftString(values, descriptor.notBefore));\n if (earlier === undefined || Number.isNaN(start.getTime())) return undefined;\n return new Date(raw) < start\n ? messages.notBefore(labelFor(descriptor), labelFor(earlier))\n : undefined;\n}\n\n/**\n * The metadata rule for one filled scalar draft value, or `undefined` when it\n * passes.\n *\n * Extracted so a row's cell is judged by the **same** rule as a record's field.\n * Two copies is how a child's `number` member would quietly start accepting\n * something its master's would reject.\n */\nfunction formatMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {\n return messages.number(labelFor(descriptor));\n }\n if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {\n return messages.date(labelFor(descriptor));\n }\n if (\n descriptor.type === 'enum' &&\n descriptor.enumValues &&\n !descriptor.enumValues.includes(raw)\n ) {\n return messages.option(labelFor(descriptor));\n }\n return undefined;\n}\n\n/**\n * The rules an owned collection carries, at both of its levels.\n *\n * **`required` on the collection means \"at least one row\"** — the only thing a\n * master can assert about a collection it owns, and a genuinely different fact\n * from `required` on a child member, which is per row. Conflating the two would\n * make an order with three lines, one of them blank, indistinguishable from an\n * order with none.\n *\n * A row's own members are judged against the child's descriptors and reported at\n * `items[2].quantity`, so the grid can put the message in the cell that caused\n * it. Read-only child members are skipped for the reason they always are: no\n * editor exists for them, so no rule the user could satisfy applies.\n */\nfunction validateComposition(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n const rows = readRowDrafts(values[descriptor.name]);\n\n if (descriptor.required && rows.length === 0) {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n return errors;\n }\n const counted = itemCountMessage(descriptor, rows.length, messages, labelFor);\n if (counted !== undefined) errors[descriptor.name] = counted;\n if (descriptor.childType === undefined) return errors;\n\n const columns = editableChildColumns(\n describeChildColumns(descriptor.childType),\n );\n\n rows.forEach((row, index) => {\n for (const column of columns) {\n if (isNeverEdited(column)) continue;\n\n const raw = row[column.name] ?? '';\n const path = rowFieldPath(descriptor.name, index, column.name);\n\n if (column.required && raw.trim() === '') {\n errors[path] = messages.required(labelFor(column));\n continue;\n }\n if (raw === '') continue;\n\n const message = formatMessage(column, raw, messages, labelFor);\n if (message !== undefined) errors[path] = message;\n }\n });\n\n return errors;\n}\n\n/**\n * The error-map key an issue belongs to — the **whole** path, not its head.\n *\n * It used to read `issue.path?.[0]`, which was right while every member was a\n * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`\n * collapsed to `'items'`, so a rule about the third line's quantity reported\n * itself against the collection and the grid had no way to know which cell it\n * meant. Joining instead produces `items[2].quantity`, which is the key\n * `validateComposition` already writes — so a schema rule and a metadata rule\n * address the same cell the same way and `composeEntityFormErrors` can merge\n * them.\n *\n * A one-segment path still yields exactly the head, so no existing rule changes\n * meaning.\n */\nfunction issueFieldName(issue: StandardSchemaV1Issue): string | undefined {\n const path = issue.path;\n if (path == null || path.length === 0) return undefined;\n\n const segments = path.map(segment => {\n const raw =\n typeof segment === 'object' && segment !== null && 'key' in segment\n ? segment.key\n : segment;\n return typeof raw === 'number' ? raw : String(raw);\n });\n\n return joinFieldPath(segments);\n}\n\n/**\n * Runs a Standard Schema over the draft and sorts its issues into per-field\n * messages plus a form-level one (an issue with no path — a cross-field rule).\n *\n * **Schemas are authored against the string draft**, so they coerce\n * (`z.coerce.number()`) rather than expect typed values: the draft is what the\n * inputs round-trip, and the same schema then validates exactly what the user\n * sees. Only the first issue per field survives, matching how a row renders.\n *\n * Synchronous rules only. An async schema returns a Promise the form's\n * synchronous validation pass cannot wait on, so it is rejected loudly rather\n * than silently passing.\n */\nexport function readSchemaIssues(\n schema: StandardSchemaV1,\n values: EntityDraft,\n translateIssue: (message: string, field: string | undefined) => string,\n): { fields: Record<string, string>; form?: string } {\n const result = schema['~standard'].validate(values);\n\n if (result instanceof Promise) {\n throw new EntifixLogicError(\n 'useEntityForm cannot run an async schema: its validation pass is ' +\n 'synchronous. Move the asynchronous rule into the `validate` option.',\n );\n }\n if (!result.issues) return { fields: {} };\n\n const fields: Record<string, string> = {};\n let form: string | undefined;\n\n for (const issue of result.issues) {\n const name = issueFieldName(issue);\n const message = translateIssue(issue.message, name);\n\n if (name === undefined) form ??= message;\n else fields[name] ??= message;\n }\n\n return { fields, form };\n}\n\n/** Everything {@link composeEntityFormErrors} needs to judge one draft. */\nexport interface ComposeEntityFormErrorsOptions {\n descriptors: readonly EntityFieldDescriptor[];\n values: EntityDraft;\n messages: EntityDraftMessages;\n /** How a metadata message names its field. Defaults to the untranslated `label`. */\n labelFor?: EntityFieldLabeler;\n schema?: StandardSchemaV1;\n translateIssue: (message: string, field: string | undefined) => string;\n validate?: (values: EntityDraft) => Record<string, string>;\n}\n\n/**\n * The single composition point for a draft's errors: metadata rules first, then\n * the entity's schema, then the caller's `validate` callback. Later sources win\n * on conflict, so a caller can always override a message it disagrees with.\n */\nexport function composeEntityFormErrors({\n descriptors,\n values,\n messages,\n labelFor,\n schema,\n translateIssue,\n validate,\n}: ComposeEntityFormErrorsOptions): {\n fields: Record<string, string>;\n form?: string;\n} {\n const metadata = validateEntityDraft(descriptors, values, messages, labelFor);\n const schemaIssues = schema\n ? readSchemaIssues(schema, values, translateIssue)\n : { fields: {}, form: undefined };\n\n return {\n fields: { ...metadata, ...schemaIssues.fields, ...validate?.(values) },\n form: schemaIssues.form,\n };\n}\n\n/**\n * What a caller who supplies no {@link ValidationMessages} gets: the catalog key\n * itself, with the field appended.\n *\n * Honest rather than English — there is no catalog in this package to resolve\n * against — and visible enough in a form that a host notices it has wired no\n * copy. It sits beside the validator rather than in `.types.ts`, which this\n * repository keeps type-only.\n */\nexport const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages = {\n required: field => `validation.required:${field}`,\n number: field => `validation.number:${field}`,\n date: field => `validation.date:${field}`,\n option: field => `validation.option:${field}`,\n minItems: (field, count) => `validation.minItems:${field}:${count}`,\n maxItems: (field, count) => `validation.maxItems:${field}:${count}`,\n notBefore: (field, other) => `validation.notBefore:${field}:${other}`,\n locale: (field, locale) => `validation.locale:${field}:${locale}`,\n};\n"],"names":["describeChildColumns","editableChildColumns","EntifixLogicError","EntityCollectionLink","EntityLink","isLocalizedText","isRowDraftArray","joinFieldPath","locales","readDraftString","readLocalizedDraft","readRowDrafts","ROW_KEY","rowFieldPath","seededRowKey","isNeverEdited","descriptor","readonly","hasCheckableFormat","type","seedFieldValue","entity","seedRowDrafts","seedLocalizedDraft","seedScalarValue","raw","name","text","Object","fromEntries","map","locale","id","String","ids","join","Date","toISOString","slice","Array","isArray","childType","undefined","columns","row","index","draft","column","seedEntityDraft","descriptors","restoreEntityDraft","seed","persisted","restored","value","readFieldErrors","fieldMeta","errors","meta","entries","message","find","entry","labelOf","label","validateEntityDraft","values","messages","labelFor","formatMessage","assign","validateComposition","localizedMessage","required","trim","counted","isCountedList","itemCountMessage","countItems","orderMessage","missing","filter","length","first","split","item","count","minItems","maxItems","notBefore","earlier","other","start","Number","isNaN","getTime","number","date","enumValues","includes","option","rows","forEach","path","issueFieldName","issue","segments","segment","key","readSchemaIssues","schema","translateIssue","result","validate","Promise","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":";AAAA,SACEA,oBAAoB,EACpBC,oBAAoB,EACpBC,iBAAiB,EACjBC,oBAAoB,EAIpBC,UAAU,EAGVC,eAAe,EACfC,eAAe,EACfC,aAAa,EACbC,OAAO,EACPC,eAAe,EACfC,kBAAkB,EAClBC,aAAa,EACbC,OAAO,EACPC,YAAY,EACZC,YAAY,QAGP,gBAAgB;AAEvB;;;;;;;;;;CAUC,GACD,SAASC,cAAcC,UAAiC;IACtD,OAAOA,WAAWC,QAAQ;AAC5B;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASC,mBAAmBF,UAAiC;IAC3D,OACEA,WAAWG,IAAI,KAAK,UACpBH,WAAWG,IAAI,KAAK,oBACpBH,WAAWG,IAAI,KAAK;AAExB;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,eACdJ,UAAiC,EACjCK,MAAe;IAEf,IAAIL,WAAWG,IAAI,KAAK,eAAe;QACrC,OAAOG,cAAcN,YAAYK;IACnC;IACA,IAAIL,WAAWG,IAAI,KAAK,aAAa;QACnC,OAAOI,mBAAmBP,YAAYK;IACxC;IACA,OAAOG,gBAAgBR,YAAYK;AACrC;AAEA;;;;CAIC,GACD,SAASE,mBACPP,UAAiC,EACjCK,MAAe;IAEf,MAAMI,MAAOJ,0BAAD,AAACA,MAAuD,CAClEL,WAAWU,IAAI,CAChB;IACD,MAAMC,OAAOtB,gBAAgBoB,OAAOA,MAAM,CAAC;IAC3C,OAAOG,OAAOC,WAAW,CACvBrB,UAAUsB,GAAG,CAACC,CAAAA;YAAmBJ;eAAT;YAACI;aAAQJ,eAAAA,IAAI,CAACI,OAAO,YAAZJ,eAAgB;SAAG;;AAExD;AAEA,+EAA+E,GAC/E,SAASH,gBACPR,UAAiC,EACjCK,MAAe;IAEf,IAAIA,UAAU,MAAM,OAAO;IAC3B,MAAMI,MAAM,AAACJ,MAAkC,CAACL,WAAWU,IAAI,CAAC;IAChE,IAAID,OAAO,MAAM,OAAO;IACxB,IAAIA,eAAerB,YAAY,OAAOqB,IAAIO,EAAE,IAAI,OAAO,KAAKC,OAAOR,IAAIO,EAAE;IACzE,IAAIP,eAAetB,sBAAsB,OAAOsB,IAAIS,GAAG,CAACJ,GAAG,CAACG,QAAQE,IAAI,CAAC;IACzE,IAAIV,eAAeW,MAAM,OAAOX,IAAIY,WAAW,GAAGC,KAAK,CAAC,GAAG;IAC3D,IAAIC,MAAMC,OAAO,CAACf,MAAM,OAAOA,IAAIK,GAAG,CAACG,QAAQE,IAAI,CAAC;IACpD,OAAOF,OAAOR;AAChB;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASH,cACPN,UAAiC,EACjCK,MAAe;IAEf,IAAIA,UAAU,QAAQL,WAAWyB,SAAS,KAAKC,WAAW,OAAO,EAAE;IACnE,MAAMjB,MAAM,AAACJ,MAAkC,CAACL,WAAWU,IAAI,CAAC;IAChE,IAAI,CAACa,MAAMC,OAAO,CAACf,MAAM,OAAO,EAAE;IAElC,MAAMkB,UAAU3C,qBAAqBgB,WAAWyB,SAAS;IAEzD,OAAOhB,IAAIK,GAAG,CAAC,CAACc,KAAKC;QACnB,yEAAyE;QACzE,gEAAgE;QAChE,MAAMC,QAAwB;YAAE,CAAClC,QAAQ,EAAEE,aAAa+B;QAAO;QAC/D,KAAK,MAAME,UAAUJ,QAAS;YAC5BG,KAAK,CAACC,OAAOrB,IAAI,CAAC,GAAGF,gBAAgBuB,QAAQH;QAC/C;QACA,OAAOE;IACT;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASE,gBACdC,WAA6C,EAC7C5B,MAAe;IAEf,MAAMyB,QAAqB,CAAC;IAC5B,KAAK,MAAM9B,cAAciC,YAAa;QACpCH,KAAK,CAAC9B,WAAWU,IAAI,CAAC,GAAGN,eAAeJ,YAAYK;IACtD;IACA,OAAOyB;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASI,mBACdD,WAA6C,EAC7CE,IAAiB,EACjBC,SAAkC;IAElC,IAAIA,cAAcV,WAAW,OAAOS;IAEpC,MAAME,WAAwB,aAAKF;IACnC,KAAK,MAAMnC,cAAciC,YAAa;QACpC,MAAMK,QAAQF,SAAS,CAACpC,WAAWU,IAAI,CAAC;QACxC,IAAI4B,UAAUZ,WAAW;QAEzB,IAAI1B,WAAWG,IAAI,KAAK,eAAe;YACrC,IAAIb,gBAAgBgD,QAAQD,QAAQ,CAACrC,WAAWU,IAAI,CAAC,GAAG4B;YACxD;QACF;QACA,IAAItC,WAAWG,IAAI,KAAK,aAAa;YACnC,sEAAsE;YACtE,oDAAoD;YACpD,IAAId,gBAAgBiD,QAAQ;gBAC1BD,QAAQ,CAACrC,WAAWU,IAAI,CAAC,GAAG,aACvBhB,mBAAmByC,MAAMnC,WAAWU,IAAI,GACxChB,mBAAmB0C,WAAWpC,WAAWU,IAAI;YAEpD;YACA;QACF;QACA,IAAI,OAAO4B,UAAU,UAAUD,QAAQ,CAACrC,WAAWU,IAAI,CAAC,GAAG4B;IAC7D;IACA,OAAOD;AACT;AAOA;;;;;;;CAOC,GACD,OAAO,SAASE,gBACdC,SAA8D;IAE9D,MAAMC,SAAiC,CAAC;IAExC,KAAK,MAAM,CAAC/B,MAAMgC,KAAK,IAAI9B,OAAO+B,OAAO,CAACH,WAAY;YACpCE;QAAhB,MAAME,UAAUF,yBAAAA,eAAAA,KAAMD,MAAM,qBAAZC,aAAcG,IAAI,CAACC,CAAAA,QAAS,OAAOA,UAAU;QAC7D,IAAI,OAAOF,YAAY,UAAUH,MAAM,CAAC/B,KAAK,GAAGkC;IAClD;IAEA,OAAOH;AACT;AAgBA,MAAMM,UAA8B/C,CAAAA,aAAcA,WAAWgD,KAAK;AAiBlE;;;;;;;;CAQC,GACD,OAAO,SAASC,oBACdhB,WAA6C,EAC7CiB,MAAmB,EACnBC,QAA6B,EAC7BC,WAA+BL,OAAO;IAEtC,MAAMN,SAAiC,CAAC;IAExC,KAAK,MAAMzC,cAAciC,YAAa;YAkClCoB;QAjCF,IAAItD,cAAcC,aAAa;QAE/B,IAAIA,WAAWG,IAAI,KAAK,eAAe;YACrCS,OAAO0C,MAAM,CACXb,QACAc,oBAAoBvD,YAAYkD,QAAQC,UAAUC;YAEpD;QACF;QACA,IAAIpD,WAAWG,IAAI,KAAK,aAAa;YACnC,MAAMyC,UAAUY,iBAAiBxD,YAAYkD,QAAQC,UAAUC;YAC/D,IAAIR,YAAYlB,WAAWe,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGkC;YACrD;QACF;QAEA,MAAMnC,MAAMhB,gBAAgByD,QAAQlD,WAAWU,IAAI;QAEnD,IAAIV,WAAWyD,QAAQ,IAAIhD,IAAIiD,IAAI,OAAO,IAAI;YAC5CjB,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGyC,SAASM,QAAQ,CAACL,SAASpD;YACrD;QACF;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAM2D,UAAUC,cAAc5D,cAC1B6D,iBAAiB7D,YAAY8D,WAAWrD,MAAM0C,UAAUC,YACxD1B;QACJ,IAAIiC,YAAYjC,WAAW;YACzBe,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGiD;YAC1B;QACF;QACA,IAAIlD,QAAQ,MAAM,CAACP,mBAAmBF,aAAa;QAEnD,MAAM4C,WACJS,iBAAAA,cAAcrD,YAAYS,KAAK0C,UAAUC,qBAAzCC,iBACAU,aAAa/D,YAAYS,KAAKwB,aAAaiB,QAAQC,UAAUC;QAC/D,IAAIR,YAAYlB,WAAWe,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGkC;IACvD;IAEA,OAAOH;AACT;AAEA;;;;;CAKC,GACD,SAASe,iBACPxD,UAAiC,EACjCkD,MAAmB,EACnBC,QAA6B,EAC7BC,QAA4B;IAE5B,MAAMtB,QAAQpC,mBAAmBwD,QAAQlD,WAAWU,IAAI;IACxD,MAAMsD,UAAUxE,UAAUyE,MAAM,CAC9BlD,CAAAA;YAAWe;eAAD,EAACA,gBAAAA,KAAK,CAACf,OAAO,YAAbe,gBAAiB,IAAI4B,IAAI,OAAO;;IAE7C,IAAIM,QAAQE,MAAM,KAAK1E,UAAU0E,MAAM,EAAE;QACvC,OAAOlE,WAAWyD,QAAQ,GACtBN,SAASM,QAAQ,CAACL,SAASpD,eAC3B0B;IACN;IACA,MAAM,CAACyC,MAAM,GAAGH;IAChB,OAAOG,UAAUzC,YACbA,YACAyB,SAASpC,MAAM,CAACqC,SAASpD,aAAamE;AAC5C;AAEA,2EAA2E,GAC3E,SAASP,cAAc5D,UAAiC;IACtD,OACEA,WAAWG,IAAI,KAAK,sBACpBH,WAAWG,IAAI,KAAK;AAExB;AAEA,8DAA8D,GAC9D,SAAS2D,WAAWrD,GAAW;IAC7B,OAAOA,IAAI2D,KAAK,CAAC,KAAKH,MAAM,CAACI,CAAAA,OAAQA,KAAKX,IAAI,OAAO,IAAIQ,MAAM;AACjE;AAEA;;;;CAIC,GACD,SAASL,iBACP7D,UAAiC,EACjCsE,KAAa,EACbnB,QAA6B,EAC7BC,QAA4B;IAE5B,MAAM,EAAEmB,QAAQ,EAAEC,QAAQ,EAAE,GAAGxE;IAC/B,IAAIuE,aAAa7C,aAAa4C,QAAQC,UAAU;QAC9C,OAAOpB,SAASoB,QAAQ,CAACnB,SAASpD,aAAauE;IACjD;IACA,IAAIC,aAAa9C,aAAa4C,QAAQE,UAAU;QAC9C,OAAOrB,SAASqB,QAAQ,CAACpB,SAASpD,aAAawE;IACjD;IACA,OAAO9C;AACT;AAEA;;;CAGC,GACD,SAASqC,aACP/D,UAAiC,EACjCS,GAAW,EACXwB,WAA6C,EAC7CiB,MAAmB,EACnBC,QAA6B,EAC7BC,QAA4B;IAE5B,IAAIpD,WAAWyE,SAAS,KAAK/C,WAAW,OAAOA;IAC/C,MAAMgD,UAAUzC,YAAYY,IAAI,CAC9B8B,CAAAA,QAASA,MAAMjE,IAAI,KAAKV,WAAWyE,SAAS;IAE9C,MAAMG,QAAQ,IAAIxD,KAAK3B,gBAAgByD,QAAQlD,WAAWyE,SAAS;IACnE,IAAIC,YAAYhD,aAAamD,OAAOC,KAAK,CAACF,MAAMG,OAAO,KAAK,OAAOrD;IACnE,OAAO,IAAIN,KAAKX,OAAOmE,QACnBzB,SAASsB,SAAS,CAACrB,SAASpD,aAAaoD,SAASsB,YAClDhD;AACN;AAEA;;;;;;;CAOC,GACD,SAAS2B,cACPrD,UAAiC,EACjCS,GAAW,EACX0C,QAA6B,EAC7BC,QAA4B;IAE5B,IAAIpD,WAAWG,IAAI,KAAK,YAAY0E,OAAOC,KAAK,CAACD,OAAOpE,OAAO;QAC7D,OAAO0C,SAAS6B,MAAM,CAAC5B,SAASpD;IAClC;IACA,IAAIA,WAAWG,IAAI,KAAK,UAAU0E,OAAOC,KAAK,CAAC,IAAI1D,KAAKX,KAAKsE,OAAO,KAAK;QACvE,OAAO5B,SAAS8B,IAAI,CAAC7B,SAASpD;IAChC;IACA,IACEA,WAAWG,IAAI,KAAK,UACpBH,WAAWkF,UAAU,IACrB,CAAClF,WAAWkF,UAAU,CAACC,QAAQ,CAAC1E,MAChC;QACA,OAAO0C,SAASiC,MAAM,CAAChC,SAASpD;IAClC;IACA,OAAO0B;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,SAAS6B,oBACPvD,UAAiC,EACjCkD,MAAmB,EACnBC,QAA6B,EAC7BC,QAA4B;IAE5B,MAAMX,SAAiC,CAAC;IACxC,MAAM4C,OAAO1F,cAAcuD,MAAM,CAAClD,WAAWU,IAAI,CAAC;IAElD,IAAIV,WAAWyD,QAAQ,IAAI4B,KAAKnB,MAAM,KAAK,GAAG;QAC5CzB,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGyC,SAASM,QAAQ,CAACL,SAASpD;QACrD,OAAOyC;IACT;IACA,MAAMkB,UAAUE,iBAAiB7D,YAAYqF,KAAKnB,MAAM,EAAEf,UAAUC;IACpE,IAAIO,YAAYjC,WAAWe,MAAM,CAACzC,WAAWU,IAAI,CAAC,GAAGiD;IACrD,IAAI3D,WAAWyB,SAAS,KAAKC,WAAW,OAAOe;IAE/C,MAAMd,UAAU1C,qBACdD,qBAAqBgB,WAAWyB,SAAS;IAG3C4D,KAAKC,OAAO,CAAC,CAAC1D,KAAKC;QACjB,KAAK,MAAME,UAAUJ,QAAS;gBAGhBC;YAFZ,IAAI7B,cAAcgC,SAAS;YAE3B,MAAMtB,OAAMmB,mBAAAA,GAAG,CAACG,OAAOrB,IAAI,CAAC,YAAhBkB,mBAAoB;YAChC,MAAM2D,OAAO1F,aAAaG,WAAWU,IAAI,EAAEmB,OAAOE,OAAOrB,IAAI;YAE7D,IAAIqB,OAAO0B,QAAQ,IAAIhD,IAAIiD,IAAI,OAAO,IAAI;gBACxCjB,MAAM,CAAC8C,KAAK,GAAGpC,SAASM,QAAQ,CAACL,SAASrB;gBAC1C;YACF;YACA,IAAItB,QAAQ,IAAI;YAEhB,MAAMmC,UAAUS,cAActB,QAAQtB,KAAK0C,UAAUC;YACrD,IAAIR,YAAYlB,WAAWe,MAAM,CAAC8C,KAAK,GAAG3C;QAC5C;IACF;IAEA,OAAOH;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAAS+C,eAAeC,KAA4B;IAClD,MAAMF,OAAOE,MAAMF,IAAI;IACvB,IAAIA,QAAQ,QAAQA,KAAKrB,MAAM,KAAK,GAAG,OAAOxC;IAE9C,MAAMgE,WAAWH,KAAKzE,GAAG,CAAC6E,CAAAA;QACxB,MAAMlF,MACJ,OAAOkF,YAAY,YAAYA,YAAY,QAAQ,SAASA,UACxDA,QAAQC,GAAG,GACXD;QACN,OAAO,OAAOlF,QAAQ,WAAWA,MAAMQ,OAAOR;IAChD;IAEA,OAAOlB,cAAcmG;AACvB;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASG,iBACdC,MAAwB,EACxB5C,MAAmB,EACnB6C,cAAsE;IAEtE,MAAMC,SAASF,MAAM,CAAC,YAAY,CAACG,QAAQ,CAAC/C;IAE5C,IAAI8C,kBAAkBE,SAAS;QAC7B,MAAM,IAAIhH,kBACR,sEACE;IAEN;IACA,IAAI,CAAC8G,OAAOG,MAAM,EAAE,OAAO;QAAEC,QAAQ,CAAC;IAAE;IAExC,MAAMA,SAAiC,CAAC;IACxC,IAAIC;IAEJ,KAAK,MAAMZ,SAASO,OAAOG,MAAM,CAAE;YAK5BC,SAAO1F,OAAP0F;QAJL,MAAM1F,OAAO8E,eAAeC;QAC5B,MAAM7C,UAAUmD,eAAeN,MAAM7C,OAAO,EAAElC;QAE9C,IAAIA,SAASgB,WAAW2E,eAAAA,OAAAA,OAASzD;cAC5BwD,IAAAA,CAAAA,UAAAA,OAAM,CAAC1F,QAAAA,KAAK,YAAZ0F,IAAAA,OAAM,CAAC1F,MAAK,GAAKkC;IACxB;IAEA,OAAO;QAAEwD;QAAQC;IAAK;AACxB;AAcA;;;;CAIC,GACD,OAAO,SAASC,wBAAwB,EACtCrE,WAAW,EACXiB,MAAM,EACNC,QAAQ,EACRC,QAAQ,EACR0C,MAAM,EACNC,cAAc,EACdE,QAAQ,EACuB;IAI/B,MAAMM,WAAWtD,oBAAoBhB,aAAaiB,QAAQC,UAAUC;IACpE,MAAMoD,eAAeV,SACjBD,iBAAiBC,QAAQ5C,QAAQ6C,kBACjC;QAAEK,QAAQ,CAAC;QAAGC,MAAM3E;IAAU;IAElC,OAAO;QACL0E,QAAQ,aAAKG,UAAaC,aAAaJ,MAAM,EAAKH,4BAAAA,SAAW/C;QAC7DmD,MAAMG,aAAaH,IAAI;IACzB;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMI,8BAAmD;IAC9DhD,UAAUiD,CAAAA,QAAS,CAAC,oBAAoB,EAAEA,OAAO;IACjD1B,QAAQ0B,CAAAA,QAAS,CAAC,kBAAkB,EAAEA,OAAO;IAC7CzB,MAAMyB,CAAAA,QAAS,CAAC,gBAAgB,EAAEA,OAAO;IACzCtB,QAAQsB,CAAAA,QAAS,CAAC,kBAAkB,EAAEA,OAAO;IAC7CnC,UAAU,CAACmC,OAAOpC,QAAU,CAAC,oBAAoB,EAAEoC,MAAM,CAAC,EAAEpC,OAAO;IACnEE,UAAU,CAACkC,OAAOpC,QAAU,CAAC,oBAAoB,EAAEoC,MAAM,CAAC,EAAEpC,OAAO;IACnEG,WAAW,CAACiC,OAAO/B,QAAU,CAAC,qBAAqB,EAAE+B,MAAM,CAAC,EAAE/B,OAAO;IACrE5D,QAAQ,CAAC2F,OAAO3F,SAAW,CAAC,kBAAkB,EAAE2F,MAAM,CAAC,EAAE3F,QAAQ;AACnE,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-entity-link-source.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,gBAAgB,EAGtB,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"use-entity-link-source.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,gBAAgB,EAGtB,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AA2C3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAClE,MAAM,EAAE,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,EACjD,EACE,UAAU,EACV,UAAU,EACV,cAAc,GACf,EAAE,0BAA0B,CAAC,OAAO,CAAC,GACrC,gBAAgB,CAAC,OAAO,CAAC,CAuJ3B"}
|
|
@@ -19,6 +19,14 @@ const DEFAULT_DEBOUNCE_MS = 250;
|
|
|
19
19
|
* break the developer's build than to lie to them.
|
|
20
20
|
*/ function assertSearchable(config, searchProperty) {
|
|
21
21
|
const target = describeEntityColumns(config.entityConstructor).find((column)=>column.name === searchProperty);
|
|
22
|
+
if ((target == null ? void 0 : target.type) === 'localized') {
|
|
23
|
+
// Queried one locale at a time, and this hook holds no locale: the bare
|
|
24
|
+
// member is what the service would refuse.
|
|
25
|
+
throw new EntifixLogicError(`Cannot search ${config.entityConstructor.name} by "${searchProperty}": ` + 'the member is localized, and a quick search has no locale to search ' + 'it in. Point the relation at a string member with `linkSearchProperty`.', undefined, {
|
|
26
|
+
entity: config.entityConstructor.name,
|
|
27
|
+
searchProperty
|
|
28
|
+
});
|
|
29
|
+
}
|
|
22
30
|
if ((target == null ? void 0 : target.filterable) === true) return;
|
|
23
31
|
throw new EntifixLogicError(`Cannot search ${config.entityConstructor.name} by "${searchProperty}": ` + 'the member is not filterable, so the service would reject the query. ' + 'Declare it `filterable` on the target entity, or point the relation at ' + 'another member with `linkSearchProperty`.', undefined, {
|
|
24
32
|
entity: config.entityConstructor.name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"sourcesContent":["'use client';\n\nimport { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport {\n describeEntityColumns,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityLinkSource,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport {\n entityQueryKey,\n entityQueryScope,\n} from '../../query/entity-query-key.js';\nimport { useDataLoading } from '../use-data-loading/index.js';\nimport type {\n EntityLinkSourceConfig,\n UseEntityLinkSourceOptions,\n} from './use-entity-link-source.types.js';\n\nconst DEFAULT_QUICK_PAGE_SIZE = 10;\nconst DEFAULT_DEBOUNCE_MS = 250;\n\n/**\n * Fails loudly on a search property the service will reject.\n *\n * A member's `filterable` metadata is also the server-side filter allowlist, so a\n * quick search over a member that lacks it answers `400` — as an empty\n * suggestion list, which reads to the user as \"there are no brands\". Better to\n * break the developer's build than to lie to them.\n */\nfunction assertSearchable<TTarget extends Entity>(\n config: EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty: string,\n): void {\n const target = describeEntityColumns(config.entityConstructor).find(\n column => column.name === searchProperty,\n );\n if (target?.filterable === true) return;\n\n throw new EntifixLogicError(\n `Cannot search ${config.entityConstructor.name} by \"${searchProperty}\": ` +\n 'the member is not filterable, so the service would reject the query. ' +\n 'Declare it `filterable` on the target entity, or point the relation at ' +\n 'another member with `linkSearchProperty`.',\n undefined,\n { entity: config.entityConstructor.name, searchProperty },\n );\n}\n\n/**\n * Produces the {@link EntityLinkSource} a link editor runs on: the quick\n * type-ahead, the browse list, and the label of whatever key is currently held.\n *\n * This is the data half of the link mechanism. It exists as a hook — rather than\n * inside the input — because `@entifix/react-controls` may not import this package\n * (both are T3, and the tier register declares no sideways edge between them), so\n * the two meet at the framework-free port declared in `@entifix/core`. A form\n * wrapper calls it once per relation and hands the results to `EntityForm`'s\n * `linkSources`.\n *\n * What may be picked is never decided here: the caller's `loadUc` (and optional\n * `baseFiltering`) is what restricts the set, so a domain rule about assignable\n * targets lives in a use-case, not in the UI.\n *\n * Three requests, each held back until it means something: the quick search runs\n * a small page and re-runs on a debounced term; the browse list does not fetch at\n * all until the dialog opens; and the label is only looked up when the caller has\n * an id but no instance — a draft restored from storage — because a picked target\n * already knows its own name.\n */\nexport function useEntityLinkSource<TTarget extends Entity, TContext>(\n config: EntityLinkSourceConfig<TTarget, TContext>,\n {\n descriptor,\n selectedId,\n selectedEntity,\n }: UseEntityLinkSourceOptions<TTarget>,\n): EntityLinkSource<TTarget> {\n const {\n entityConstructor,\n loadUc,\n getUc,\n ctx,\n baseFiltering,\n quickPageSize = DEFAULT_QUICK_PAGE_SIZE,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n } = config;\n\n const searchProperty = descriptor.linkSearchProperty;\n assertSearchable(\n config as unknown as EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty,\n );\n\n // Callers rebuild `loadUc`/`getUc`/`ctx` inline on every render, so the latest\n // are held in refs and kept out of the query keys — keying on their identity\n // would refetch on every render. Same reason and same shape as `useDataLoading`.\n const effects = useRef({ loadUc, getUc, ctx });\n useEffect(() => {\n effects.current = { loadUc, getUc, ctx };\n });\n\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useState('');\n const [isOpen, setOpen] = useState(false);\n\n // Typing is not a request per keystroke: the term settles first, and only the\n // settled value reaches the service.\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedTerm(term), debounceMs);\n return () => clearTimeout(timer);\n }, [term, debounceMs]);\n\n const quickRequest = useMemo<EntityLoadRequest<TTarget>>(() => {\n const filtering = combineFilterGroups<TTarget>(\n baseFiltering,\n debouncedTerm === ''\n ? undefined\n : {\n operator: 'and',\n values: [\n {\n property: searchProperty as keyof TTarget,\n operator: 'like',\n value: debouncedTerm,\n },\n ],\n },\n );\n\n const request: EntityLoadRequest<TTarget> = {\n page: 1,\n pageSize: quickPageSize,\n };\n if (filtering) request.filtering = [filtering];\n return request;\n }, [baseFiltering, debouncedTerm, quickPageSize, searchProperty]);\n\n const quickQuery = useQuery<EntityPage<TTarget>, EntifixError>({\n queryKey: [\n ...entityQueryKey(entityConstructor, quickRequest),\n 'link-quick',\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n effects.current.loadUc,\n effects.current.ctx.pipe(\n Context.add(\n EntityLoadRequestTag,\n // The tag carries no generic, so the entity-typed request crosses\n // it — the same cast `loadUCFactory` makes reading it back.\n quickRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const browse = useDataLoading<TTarget, TContext>({\n uc: loadUc,\n ctx,\n queryKey: entityQueryScope(entityConstructor),\n baseFiltering,\n enabled: isOpen,\n });\n\n const needsLabel =\n selectedEntity === undefined && selectedId != null && getUc !== undefined;\n\n const labelQuery = useQuery<TTarget, EntifixError>({\n queryKey: [\n ...entityQueryScope(entityConstructor),\n 'link-label',\n String(selectedId),\n ],\n enabled: needsLabel,\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n // Guarded by `enabled`: the query only runs when both are present.\n effects.current.getUc as NonNullable<typeof getUc>,\n effects.current.ctx.pipe(Context.add(EntityIdTag, selectedId)),\n ),\n ),\n });\n\n // A blank label is as useless as a missing one, so both fall back to the key —\n // an unnamed row must still be distinguishable from the row above it.\n const labelOf = useCallback(\n (target: TTarget) => {\n const label = (target as Record<string, unknown>)[\n descriptor.linkLabelProperty\n ];\n return label == null || label === ''\n ? String(target.id ?? '')\n : String(label);\n },\n [descriptor.linkLabelProperty],\n );\n\n const resolved = selectedEntity ?? labelQuery.data;\n\n return {\n entityConstructor,\n labelOf,\n selected: {\n label: resolved === undefined ? undefined : labelOf(resolved),\n // The whole target, not only its name. The lookup above already fetched\n // it, and a form restoring a draft has nothing else to rebuild an\n // `embedded` relation from — the draft kept the id and lost the instance.\n entity: resolved,\n isLoading: needsLabel && labelQuery.isFetching,\n },\n quick: {\n term,\n setTerm,\n options: quickQuery.data?.items ?? [],\n isLoading: quickQuery.isFetching,\n error: quickQuery.error ?? undefined,\n },\n browse: {\n ...browse,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n },\n };\n}\n"],"names":["EntityIdTag","EntityLoadRequestTag","describeEntityColumns","EntifixLogicError","useQuery","Context","Effect","useCallback","useEffect","useMemo","useRef","useState","combineFilterGroups","entityQueryKey","entityQueryScope","useDataLoading","DEFAULT_QUICK_PAGE_SIZE","DEFAULT_DEBOUNCE_MS","assertSearchable","config","searchProperty","target","entityConstructor","find","column","name","filterable","undefined","entity","useEntityLinkSource","descriptor","selectedId","selectedEntity","quickQuery","loadUc","getUc","ctx","baseFiltering","quickPageSize","debounceMs","linkSearchProperty","effects","current","term","setTerm","debouncedTerm","setDebouncedTerm","isOpen","setOpen","timer","setTimeout","clearTimeout","quickRequest","filtering","operator","values","property","value","request","page","pageSize","queryKey","queryFn","runPromise","provide","pipe","add","browse","uc","enabled","needsLabel","labelQuery","String","labelOf","label","linkLabelProperty","id","resolved","data","selected","isLoading","isFetching","quick","options","items","error","open","close"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"sourcesContent":["'use client';\n\nimport { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport {\n describeEntityColumns,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityLinkSource,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport {\n entityQueryKey,\n entityQueryScope,\n} from '../../query/entity-query-key.js';\nimport { useDataLoading } from '../use-data-loading/index.js';\nimport type {\n EntityLinkSourceConfig,\n UseEntityLinkSourceOptions,\n} from './use-entity-link-source.types.js';\n\nconst DEFAULT_QUICK_PAGE_SIZE = 10;\nconst DEFAULT_DEBOUNCE_MS = 250;\n\n/**\n * Fails loudly on a search property the service will reject.\n *\n * A member's `filterable` metadata is also the server-side filter allowlist, so a\n * quick search over a member that lacks it answers `400` — as an empty\n * suggestion list, which reads to the user as \"there are no brands\". Better to\n * break the developer's build than to lie to them.\n */\nfunction assertSearchable<TTarget extends Entity>(\n config: EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty: string,\n): void {\n const target = describeEntityColumns(config.entityConstructor).find(\n column => column.name === searchProperty,\n );\n if (target?.type === 'localized') {\n // Queried one locale at a time, and this hook holds no locale: the bare\n // member is what the service would refuse.\n throw new EntifixLogicError(\n `Cannot search ${config.entityConstructor.name} by \"${searchProperty}\": ` +\n 'the member is localized, and a quick search has no locale to search ' +\n 'it in. Point the relation at a string member with `linkSearchProperty`.',\n undefined,\n { entity: config.entityConstructor.name, searchProperty },\n );\n }\n if (target?.filterable === true) return;\n\n throw new EntifixLogicError(\n `Cannot search ${config.entityConstructor.name} by \"${searchProperty}\": ` +\n 'the member is not filterable, so the service would reject the query. ' +\n 'Declare it `filterable` on the target entity, or point the relation at ' +\n 'another member with `linkSearchProperty`.',\n undefined,\n { entity: config.entityConstructor.name, searchProperty },\n );\n}\n\n/**\n * Produces the {@link EntityLinkSource} a link editor runs on: the quick\n * type-ahead, the browse list, and the label of whatever key is currently held.\n *\n * This is the data half of the link mechanism. It exists as a hook — rather than\n * inside the input — because `@entifix/react-controls` may not import this package\n * (both are T3, and the tier register declares no sideways edge between them), so\n * the two meet at the framework-free port declared in `@entifix/core`. A form\n * wrapper calls it once per relation and hands the results to `EntityForm`'s\n * `linkSources`.\n *\n * What may be picked is never decided here: the caller's `loadUc` (and optional\n * `baseFiltering`) is what restricts the set, so a domain rule about assignable\n * targets lives in a use-case, not in the UI.\n *\n * Three requests, each held back until it means something: the quick search runs\n * a small page and re-runs on a debounced term; the browse list does not fetch at\n * all until the dialog opens; and the label is only looked up when the caller has\n * an id but no instance — a draft restored from storage — because a picked target\n * already knows its own name.\n */\nexport function useEntityLinkSource<TTarget extends Entity, TContext>(\n config: EntityLinkSourceConfig<TTarget, TContext>,\n {\n descriptor,\n selectedId,\n selectedEntity,\n }: UseEntityLinkSourceOptions<TTarget>,\n): EntityLinkSource<TTarget> {\n const {\n entityConstructor,\n loadUc,\n getUc,\n ctx,\n baseFiltering,\n quickPageSize = DEFAULT_QUICK_PAGE_SIZE,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n } = config;\n\n const searchProperty = descriptor.linkSearchProperty;\n assertSearchable(\n config as unknown as EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty,\n );\n\n // Callers rebuild `loadUc`/`getUc`/`ctx` inline on every render, so the latest\n // are held in refs and kept out of the query keys — keying on their identity\n // would refetch on every render. Same reason and same shape as `useDataLoading`.\n const effects = useRef({ loadUc, getUc, ctx });\n useEffect(() => {\n effects.current = { loadUc, getUc, ctx };\n });\n\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useState('');\n const [isOpen, setOpen] = useState(false);\n\n // Typing is not a request per keystroke: the term settles first, and only the\n // settled value reaches the service.\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedTerm(term), debounceMs);\n return () => clearTimeout(timer);\n }, [term, debounceMs]);\n\n const quickRequest = useMemo<EntityLoadRequest<TTarget>>(() => {\n const filtering = combineFilterGroups<TTarget>(\n baseFiltering,\n debouncedTerm === ''\n ? undefined\n : {\n operator: 'and',\n values: [\n {\n property: searchProperty as keyof TTarget,\n operator: 'like',\n value: debouncedTerm,\n },\n ],\n },\n );\n\n const request: EntityLoadRequest<TTarget> = {\n page: 1,\n pageSize: quickPageSize,\n };\n if (filtering) request.filtering = [filtering];\n return request;\n }, [baseFiltering, debouncedTerm, quickPageSize, searchProperty]);\n\n const quickQuery = useQuery<EntityPage<TTarget>, EntifixError>({\n queryKey: [\n ...entityQueryKey(entityConstructor, quickRequest),\n 'link-quick',\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n effects.current.loadUc,\n effects.current.ctx.pipe(\n Context.add(\n EntityLoadRequestTag,\n // The tag carries no generic, so the entity-typed request crosses\n // it — the same cast `loadUCFactory` makes reading it back.\n quickRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const browse = useDataLoading<TTarget, TContext>({\n uc: loadUc,\n ctx,\n queryKey: entityQueryScope(entityConstructor),\n baseFiltering,\n enabled: isOpen,\n });\n\n const needsLabel =\n selectedEntity === undefined && selectedId != null && getUc !== undefined;\n\n const labelQuery = useQuery<TTarget, EntifixError>({\n queryKey: [\n ...entityQueryScope(entityConstructor),\n 'link-label',\n String(selectedId),\n ],\n enabled: needsLabel,\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n // Guarded by `enabled`: the query only runs when both are present.\n effects.current.getUc as NonNullable<typeof getUc>,\n effects.current.ctx.pipe(Context.add(EntityIdTag, selectedId)),\n ),\n ),\n });\n\n // A blank label is as useless as a missing one, so both fall back to the key —\n // an unnamed row must still be distinguishable from the row above it.\n const labelOf = useCallback(\n (target: TTarget) => {\n const label = (target as Record<string, unknown>)[\n descriptor.linkLabelProperty\n ];\n return label == null || label === ''\n ? String(target.id ?? '')\n : String(label);\n },\n [descriptor.linkLabelProperty],\n );\n\n const resolved = selectedEntity ?? labelQuery.data;\n\n return {\n entityConstructor,\n labelOf,\n selected: {\n label: resolved === undefined ? undefined : labelOf(resolved),\n // The whole target, not only its name. The lookup above already fetched\n // it, and a form restoring a draft has nothing else to rebuild an\n // `embedded` relation from — the draft kept the id and lost the instance.\n entity: resolved,\n isLoading: needsLabel && labelQuery.isFetching,\n },\n quick: {\n term,\n setTerm,\n options: quickQuery.data?.items ?? [],\n isLoading: quickQuery.isFetching,\n error: quickQuery.error ?? undefined,\n },\n browse: {\n ...browse,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n },\n };\n}\n"],"names":["EntityIdTag","EntityLoadRequestTag","describeEntityColumns","EntifixLogicError","useQuery","Context","Effect","useCallback","useEffect","useMemo","useRef","useState","combineFilterGroups","entityQueryKey","entityQueryScope","useDataLoading","DEFAULT_QUICK_PAGE_SIZE","DEFAULT_DEBOUNCE_MS","assertSearchable","config","searchProperty","target","entityConstructor","find","column","name","type","undefined","entity","filterable","useEntityLinkSource","descriptor","selectedId","selectedEntity","quickQuery","loadUc","getUc","ctx","baseFiltering","quickPageSize","debounceMs","linkSearchProperty","effects","current","term","setTerm","debouncedTerm","setDebouncedTerm","isOpen","setOpen","timer","setTimeout","clearTimeout","quickRequest","filtering","operator","values","property","value","request","page","pageSize","queryKey","queryFn","runPromise","provide","pipe","add","browse","uc","enabled","needsLabel","labelQuery","String","labelOf","label","linkLabelProperty","id","resolved","data","selected","isLoading","isFetching","quick","options","items","error","open","close"],"mappings":"AAAA;;AAEA,SAASA,WAAW,EAAEC,oBAAoB,QAAQ,oBAAoB;AACtE,SACEC,qBAAqB,EAErBC,iBAAiB,QAKZ,gBAAgB;AACvB,SAASC,QAAQ,QAAQ,wBAAwB;AACjD,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAE1E,SAASC,mBAAmB,QAAQ,mCAAmC;AACvE,SACEC,cAAc,EACdC,gBAAgB,QACX,kCAAkC;AACzC,SAASC,cAAc,QAAQ,+BAA+B;AAM9D,MAAMC,0BAA0B;AAChC,MAAMC,sBAAsB;AAE5B;;;;;;;CAOC,GACD,SAASC,iBACPC,MAAgD,EAChDC,cAAsB;IAEtB,MAAMC,SAASnB,sBAAsBiB,OAAOG,iBAAiB,EAAEC,IAAI,CACjEC,CAAAA,SAAUA,OAAOC,IAAI,KAAKL;IAE5B,IAAIC,CAAAA,0BAAAA,OAAQK,IAAI,MAAK,aAAa;QAChC,wEAAwE;QACxE,2CAA2C;QAC3C,MAAM,IAAIvB,kBACR,CAAC,cAAc,EAAEgB,OAAOG,iBAAiB,CAACG,IAAI,CAAC,KAAK,EAAEL,eAAe,GAAG,CAAC,GACvE,yEACA,2EACFO,WACA;YAAEC,QAAQT,OAAOG,iBAAiB,CAACG,IAAI;YAAEL;QAAe;IAE5D;IACA,IAAIC,CAAAA,0BAAAA,OAAQQ,UAAU,MAAK,MAAM;IAEjC,MAAM,IAAI1B,kBACR,CAAC,cAAc,EAAEgB,OAAOG,iBAAiB,CAACG,IAAI,CAAC,KAAK,EAAEL,eAAe,GAAG,CAAC,GACvE,0EACA,4EACA,6CACFO,WACA;QAAEC,QAAQT,OAAOG,iBAAiB,CAACG,IAAI;QAAEL;IAAe;AAE5D;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASU,oBACdX,MAAiD,EACjD,EACEY,UAAU,EACVC,UAAU,EACVC,cAAc,EACsB;cA+I3BC;QAFEA;IA3Ib,MAAM,EACJZ,iBAAiB,EACjBa,MAAM,EACNC,KAAK,EACLC,GAAG,EACHC,aAAa,EACbC,gBAAgBvB,uBAAuB,EACvCwB,aAAavB,mBAAmB,EACjC,GAAGE;IAEJ,MAAMC,iBAAiBW,WAAWU,kBAAkB;IACpDvB,iBACEC,QACAC;IAGF,+EAA+E;IAC/E,6EAA6E;IAC7E,iFAAiF;IACjF,MAAMsB,UAAUhC,OAAO;QAAEyB;QAAQC;QAAOC;IAAI;IAC5C7B,UAAU;QACRkC,QAAQC,OAAO,GAAG;YAAER;YAAQC;YAAOC;QAAI;IACzC;IAEA,MAAM,CAACO,MAAMC,QAAQ,GAAGlC,SAAS;IACjC,MAAM,CAACmC,eAAeC,iBAAiB,GAAGpC,SAAS;IACnD,MAAM,CAACqC,QAAQC,QAAQ,GAAGtC,SAAS;IAEnC,8EAA8E;IAC9E,qCAAqC;IACrCH,UAAU;QACR,MAAM0C,QAAQC,WAAW,IAAMJ,iBAAiBH,OAAOJ;QACvD,OAAO,IAAMY,aAAaF;IAC5B,GAAG;QAACN;QAAMJ;KAAW;IAErB,MAAMa,eAAe5C,QAAoC;QACvD,MAAM6C,YAAY1C,oBAChB0B,eACAQ,kBAAkB,KACdnB,YACA;YACE4B,UAAU;YACVC,QAAQ;gBACN;oBACEC,UAAUrC;oBACVmC,UAAU;oBACVG,OAAOZ;gBACT;aACD;QACH;QAGN,MAAMa,UAAsC;YAC1CC,MAAM;YACNC,UAAUtB;QACZ;QACA,IAAIe,WAAWK,QAAQL,SAAS,GAAG;YAACA;SAAU;QAC9C,OAAOK;IACT,GAAG;QAACrB;QAAeQ;QAAeP;QAAenB;KAAe;IAEhE,MAAMc,aAAa9B,SAA4C;QAC7D0D,UAAU;eACLjD,eAAeS,mBAAmB+B;YACrC;SACD;QACDU,SAAS,IACPzD,OAAO0D,UAAU,CACf1D,OAAO2D,OAAO,CACZvB,QAAQC,OAAO,CAACR,MAAM,EACtBO,QAAQC,OAAO,CAACN,GAAG,CAAC6B,IAAI,CACtB7D,QAAQ8D,GAAG,CACTlE,sBACA,kEAAkE;YAClE,4DAA4D;YAC5DoD;IAKZ;IAEA,MAAMe,SAASrD,eAAkC;QAC/CsD,IAAIlC;QACJE;QACAyB,UAAUhD,iBAAiBQ;QAC3BgB;QACAgC,SAAStB;IACX;IAEA,MAAMuB,aACJtC,mBAAmBN,aAAaK,cAAc,QAAQI,UAAUT;IAElE,MAAM6C,aAAapE,SAAgC;QACjD0D,UAAU;eACLhD,iBAAiBQ;YACpB;YACAmD,OAAOzC;SACR;QACDsC,SAASC;QACTR,SAAS,IACPzD,OAAO0D,UAAU,CACf1D,OAAO2D,OAAO,CACZ,mEAAmE;YACnEvB,QAAQC,OAAO,CAACP,KAAK,EACrBM,QAAQC,OAAO,CAACN,GAAG,CAAC6B,IAAI,CAAC7D,QAAQ8D,GAAG,CAACnE,aAAagC;IAG1D;IAEA,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM0C,UAAUnE,YACd,CAACc;YAKYA;QAJX,MAAMsD,QAAQ,AAACtD,MAAkC,CAC/CU,WAAW6C,iBAAiB,CAC7B;QACD,OAAOD,SAAS,QAAQA,UAAU,KAC9BF,QAAOpD,aAAAA,OAAOwD,EAAE,YAATxD,aAAa,MACpBoD,OAAOE;IACb,GACA;QAAC5C,WAAW6C,iBAAiB;KAAC;IAGhC,MAAME,WAAW7C,yBAAAA,iBAAkBuC,WAAWO,IAAI;IAElD,OAAO;QACLzD;QACAoD;QACAM,UAAU;YACRL,OAAOG,aAAanD,YAAYA,YAAY+C,QAAQI;YACpD,wEAAwE;YACxE,kEAAkE;YAClE,0EAA0E;YAC1ElD,QAAQkD;YACRG,WAAWV,cAAcC,WAAWU,UAAU;QAChD;QACAC,OAAO;YACLvC;YACAC;YACAuC,OAAO,WAAElD,mBAAAA,WAAW6C,IAAI,qBAAf7C,iBAAiBmD,KAAK,mBAAI,EAAE;YACrCJ,WAAW/C,WAAWgD,UAAU;YAChCI,KAAK,GAAEpD,oBAAAA,WAAWoD,KAAK,YAAhBpD,oBAAoBP;QAC7B;QACAyC,QAAQ,aACHA;YACHpB;YACAuC,MAAM,IAAMtC,QAAQ;YACpBuC,OAAO,IAAMvC,QAAQ;;IAEzB;AACF"}
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
|
-
"version": "0.
|
|
24
|
+
"version": "0.4.0",
|
|
25
25
|
"description": "React hooks that bind entities to data: TanStack Query and Form over the entity adapters, drafts, mutations and live invalidation.",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": {
|
|
@@ -39,13 +39,11 @@
|
|
|
39
39
|
"exports": {
|
|
40
40
|
"./package.json": "./package.json",
|
|
41
41
|
".": {
|
|
42
|
-
"@entifix/source": "./src/index.ts",
|
|
43
42
|
"types": "./dist/index.d.ts",
|
|
44
43
|
"import": "./dist/index.js",
|
|
45
44
|
"default": "./dist/index.js"
|
|
46
45
|
},
|
|
47
46
|
"./url": {
|
|
48
|
-
"@entifix/source": "./src/url.ts",
|
|
49
47
|
"types": "./dist/url.d.ts",
|
|
50
48
|
"import": "./dist/url.js",
|
|
51
49
|
"default": "./dist/url.js"
|
|
@@ -57,16 +55,16 @@
|
|
|
57
55
|
"LICENSE"
|
|
58
56
|
],
|
|
59
57
|
"dependencies": {
|
|
60
|
-
"@entifix/
|
|
61
|
-
"@entifix/
|
|
62
|
-
"@entifix/
|
|
58
|
+
"@entifix/business": "0.4.0",
|
|
59
|
+
"@entifix/core": "0.4.0",
|
|
60
|
+
"@entifix/transactions": "0.4.0"
|
|
63
61
|
},
|
|
64
62
|
"devDependencies": {
|
|
65
63
|
"@tanstack/react-form": "1.33.5",
|
|
66
|
-
"@tanstack/react-query": "^5.
|
|
64
|
+
"@tanstack/react-query": "^5.104.5",
|
|
67
65
|
"effect": "4.0.5",
|
|
68
|
-
"react": "19.
|
|
69
|
-
"@entifix/testing-unit": "0.
|
|
66
|
+
"react": "19.3.1",
|
|
67
|
+
"@entifix/testing-unit": "0.4.0"
|
|
70
68
|
},
|
|
71
69
|
"peerDependencies": {
|
|
72
70
|
"@tanstack/react-form": ">=1.33 <2",
|