@entifix/next-shell 0.2.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAIrC,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,MAAM;IACzD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,EACrD,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,GACV,EAAE,mBAAmB,CAAC,OAAO,CAAC,+BA8H9B"}
1
+ {"version":3,"file":"entity-crud-form.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/entity-crud-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAKrC,MAAM,WAAW,mBAAmB,CAAC,OAAO,SAAS,MAAM;IACzD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,EACrD,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,GACV,EAAE,mBAAmB,CAAC,OAAO,CAAC,+BAiH9B"}
@@ -7,6 +7,7 @@ import { useEntityForm } from '@entifix/react-integration';
7
7
  import { useEffect, useMemo } from 'react';
8
8
  import { useEntityAffordances } from './use-entity-affordances.js';
9
9
  import { useEntityLinkSources } from './use-entity-link-sources.js';
10
+ import { validationMessagesFor } from './validation-messages.js';
10
11
  /**
11
12
  * The generated create/update form. A thin wrapper over the agnostic
12
13
  * {@link EntityForm}: every field, label and validation rule comes from the
@@ -24,21 +25,8 @@ import { useEntityLinkSources } from './use-entity-link-sources.js';
24
25
  */ export function EntityCrudForm({ entityConstructor, catalogKey, hiddenFields, links, entity, isLoading = false, isSaving = false, isDeleting = false, error, onSave, onDelete, backHref, renderLink, draft, metadataSource, onUseCase }) {
25
26
  const et = useT('entity');
26
27
  const ct = useT('controls');
27
- // `controls` copy, resolved here and handed down. The integration package
28
- // owns the validation *rules* and this shell owns the sentences, because a
29
- // sideways import between the two T3 packages is an edge the tier register
30
- // does not declare.
31
- const validationMessages = useMemo(()=>{
32
- const say = (failure)=>(field)=>ct(`validation.${failure}`, {
33
- field
34
- });
35
- return {
36
- required: say('required'),
37
- number: say('number'),
38
- date: say('date'),
39
- option: say('option')
40
- };
41
- }, [
28
+ // `controls` copy, resolved here and handed down; see validationMessagesFor.
29
+ const validationMessages = useMemo(()=>validationMessagesFor(ct), [
42
30
  ct
43
31
  ]);
44
32
  // A runtime-key translator, so a message names the field by its catalog word
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport {\n EntityField,\n EntityForm,\n type RenderLink,\n useT,\n useTranslateKey,\n} from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /** Renders the Back link; see `useCrudRenderLink`. */\n readonly renderLink?: RenderLink;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n renderLink,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down. The integration package\n // owns the validation *rules* and this shell owns the sentences, because a\n // sideways import between the two T3 packages is an edge the tier register\n // does not declare.\n const validationMessages = useMemo(() => {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n };\n }, [ct]);\n\n // A runtime-key translator, so a message names the field by its catalog word\n // (`labelKey`) and a schema's keyed message resolves at all. Without it both\n // fell back to untranslated text inside an otherwise Spanish sentence.\n const translateKey = useTranslateKey();\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n translateKey,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n renderLink={renderLink}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useTranslateKey","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","renderLink","draft","metadataSource","onUseCase","et","ct","validationMessages","say","failure","field","required","number","date","option","translateKey","form","onSubmit","values","existing","linkSources","selection","hydrateLink","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/crud/entity-crud-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityMetadataSource,\n reconstructEntity,\n} from '@entifix/core';\nimport {\n EntityField,\n EntityForm,\n type RenderLink,\n useT,\n useTranslateKey,\n} from '@entifix/react-controls';\nimport {\n type EntityDraftStore,\n useEntityForm,\n} from '@entifix/react-integration';\nimport { useEffect, useMemo } from 'react';\n\nimport type {\n EntityCatalogKey,\n EntityCrudLinkSource,\n} from './make-entity-crud.types.js';\nimport { useEntityAffordances } from './use-entity-affordances.js';\nimport { useEntityLinkSources } from './use-entity-link-sources.js';\nimport { validationMessagesFor } from './validation-messages.js';\n\nexport interface EntityCrudFormProps<TEntity extends Entity> {\n readonly entityConstructor: EntityConstructor<TEntity>;\n readonly catalogKey: EntityCatalogKey;\n readonly hiddenFields: readonly string[];\n readonly links: readonly EntityCrudLinkSource[];\n /** The record being edited; `undefined` means this is a create. */\n readonly entity?: TEntity;\n readonly isLoading?: boolean;\n readonly isSaving?: boolean;\n readonly isDeleting?: boolean;\n readonly error?: EntifixError;\n readonly onSave: (entity: TEntity) => void;\n /** Omitted for a create — there is nothing to delete yet. */\n readonly onDelete?: () => void;\n readonly backHref: string;\n /** Renders the Back link; see `useCrudRenderLink`. */\n readonly renderLink?: RenderLink;\n /**\n * Where to autosave this form's draft. Omitted by a route host, which is what\n * keeps a plain route ephemeral; the workspace tab host supplies one.\n */\n readonly draft?: EntityDraftStore;\n /**\n * Where this caller's affordances come from. Absent keeps the pre-ADR-0026\n * behaviour: Save and Delete render unconditionally and no declared verb\n * appears — which is what every generated catalog did before it existed.\n */\n readonly metadataSource?: EntityMetadataSource;\n /** Runs a declared `entity`-bound verb on this record, by its key. */\n readonly onUseCase?: (key: string) => void;\n}\n\n/**\n * The generated create/update form. A thin wrapper over the agnostic\n * {@link EntityForm}: every field, label and validation rule comes from the\n * entity's own accessor metadata, and `reconstructEntity` derives the submit\n * rebuild from that same metadata — so nothing here is per-entity but the\n * constructor, the catalog key and the hidden-field list.\n *\n * A hidden member still round-trips. `code` is assigned by the create\n * transaction and never shown, but it is a plain writable member, so the rebuild\n * picks it out of the seeded draft; dropping it from the rendered fields is not\n * dropping it from the draft.\n *\n * `useEntityForm` owns the draft and seeds it once, so the page keys this\n * component by the record id to reseed when the record arrives.\n */\nexport function EntityCrudForm<TEntity extends Entity>({\n entityConstructor,\n catalogKey,\n hiddenFields,\n links,\n entity,\n isLoading = false,\n isSaving = false,\n isDeleting = false,\n error,\n onSave,\n onDelete,\n backHref,\n renderLink,\n draft,\n metadataSource,\n onUseCase,\n}: EntityCrudFormProps<TEntity>) {\n const et = useT('entity');\n const ct = useT('controls');\n\n // `controls` copy, resolved here and handed down; see validationMessagesFor.\n const validationMessages = useMemo(() => validationMessagesFor(ct), [ct]);\n\n // A runtime-key translator, so a message names the field by its catalog word\n // (`labelKey`) and a schema's keyed message resolves at all. Without it both\n // fell back to untranslated text inside an otherwise Spanish sentence.\n const translateKey = useTranslateKey();\n\n const form = useEntityForm<TEntity>({\n entityConstructor,\n entity,\n validationMessages,\n translateKey,\n draft,\n onSubmit: values =>\n onSave(\n reconstructEntity(entityConstructor, values, { existing: entity }),\n ),\n });\n\n const linkSources = useEntityLinkSources(links, {\n values: form.values,\n selection: form.links,\n });\n\n // Refill the relation sidecar from the ids the draft did keep.\n //\n // A draft is JSON, so it holds ids and nothing else; the instances behind them\n // die with the page. Each picker already resolves its id — that is how a\n // restored draft shows a brand's name instead of its key — so the instance is\n // in hand and only has to be handed back. Without this, a member declared\n // `linkSerialization: 'embedded'` reaches `applyEntityLinks` with an id and no\n // target, which it refuses rather than writing the wrong wire shape.\n //\n // `hydrateLink`, not `setLink`: this is a lookup landing, not a pick, so it\n // must leave the draft — and the form's dirty flag — alone.\n const { links: selection, hydrateLink } = form;\n useEffect(() => {\n for (const [field, source] of Object.entries(linkSources)) {\n const resolved = source.selected.entity;\n if (resolved !== undefined && selection[field] === undefined) {\n hydrateLink(field, resolved);\n }\n }\n }, [linkSources, selection, hydrateLink]);\n\n const affordances = useEntityAffordances(entityConstructor, metadataSource);\n\n /**\n * May this caller write the record in front of them?\n *\n * The **same authority that withholds Save**, applied one step earlier.\n * Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n * site is untouched; present metadata is authoritative, because it is what\n * the service already decided.\n *\n * ⚠️ Without this the form rendered inputs nobody could submit. Measured on\n * the stock surface: `stock-item` answers `[\"read\"]` — no role holds\n * `stock-item:write` and there is no save route — so Save was correctly\n * absent while `onHand` and `reserved` still rendered as **enabled number\n * inputs**. A vendor could type a new quantity into the very field ADR 0010\n * forbids writing, and nothing would happen and nothing would say why.\n *\n * A create is exempt: there is no record yet, so there is nothing to read and\n * a `read` mode would render an empty form with no way to fill it.\n */\n const mayWrite =\n entity === undefined ||\n affordances.metadata === undefined ||\n affordances.metadata.actions.includes('write');\n\n return (\n <EntityForm<TEntity>\n entityConstructor={entityConstructor}\n entity={entity}\n // Driven by the served descriptor rather than fixed: `edit` for a caller\n // who may write, `read` for one who may not. Passing it at all is what\n // suppresses the built-in Ver/Editar toggle, which would otherwise offer\n // a way back into inputs the service will refuse.\n mode={mayWrite ? 'edit' : 'read'}\n values={form.values}\n onFieldChange={form.setField}\n linkSources={linkSources}\n onLinkChange={form.setLink}\n errors={form.errors}\n formError={form.formError}\n onSubmit={form.submit}\n onDelete={onDelete}\n {...affordances}\n onUseCase={onUseCase}\n isLoading={isLoading}\n isSaving={isSaving}\n isDeleting={isDeleting}\n error={error}\n backHref={backHref}\n renderLink={renderLink}\n // `entity` is undefined until the record lands, so testing it alone\n // titled a loading edit form \"New\" and then relabelled it (#139).\n title={et(\n entity || isLoading\n ? `${catalogKey}.form.editTitle`\n : `${catalogKey}.form.newTitle`,\n )}\n >\n {hiddenFields.map(field => (\n <EntityField<TEntity> key={field} field={field} hidden />\n ))}\n </EntityForm>\n );\n}\n"],"names":["reconstructEntity","EntityField","EntityForm","useT","useTranslateKey","useEntityForm","useEffect","useMemo","useEntityAffordances","useEntityLinkSources","validationMessagesFor","EntityCrudForm","entityConstructor","catalogKey","hiddenFields","links","entity","isLoading","isSaving","isDeleting","error","onSave","onDelete","backHref","renderLink","draft","metadataSource","onUseCase","et","ct","validationMessages","translateKey","form","onSubmit","values","existing","linkSources","selection","hydrateLink","field","source","Object","entries","resolved","selected","undefined","affordances","mayWrite","metadata","actions","includes","mode","onFieldChange","setField","onLinkChange","setLink","errors","formError","submit","title","map","hidden"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { EntityDraftMessages } from '@entifix/react-integration';
2
+ /** A translate function bound to the `controls` namespace. */
3
+ export type ControlsTranslate = (key: string, params?: Record<string, unknown>) => string;
4
+ /**
5
+ * The form's validation sentences, in the `controls` catalog's words.
6
+ *
7
+ * The integration package owns the validation *rules* and this shell owns the
8
+ * sentences, because a sideways import between the two T3 packages is an edge
9
+ * the tier register does not declare. `count` doubles as i18next's plural
10
+ * selector, so `minItems` reads "1 item" and "2 items".
11
+ */
12
+ export declare function validationMessagesFor(ct: ControlsTranslate): EntityDraftMessages;
13
+ //# sourceMappingURL=validation-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-messages.d.ts","sourceRoot":"","sources":["../../../src/lib/crud/validation-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,iBAAiB,GACpB,mBAAmB,CAerB"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The form's validation sentences, in the `controls` catalog's words.
3
+ *
4
+ * The integration package owns the validation *rules* and this shell owns the
5
+ * sentences, because a sideways import between the two T3 packages is an edge
6
+ * the tier register does not declare. `count` doubles as i18next's plural
7
+ * selector, so `minItems` reads "1 item" and "2 items".
8
+ */ export function validationMessagesFor(ct) {
9
+ const say = (failure)=>(field)=>ct(`validation.${failure}`, {
10
+ field
11
+ });
12
+ return {
13
+ required: say('required'),
14
+ number: say('number'),
15
+ date: say('date'),
16
+ option: say('option'),
17
+ minItems: (field, count)=>ct('validation.minItems', {
18
+ field,
19
+ count
20
+ }),
21
+ maxItems: (field, count)=>ct('validation.maxItems', {
22
+ field,
23
+ count
24
+ }),
25
+ notBefore: (field, other)=>ct('validation.notBefore', {
26
+ field,
27
+ other
28
+ }),
29
+ locale: (field, locale)=>ct('validation.locale', {
30
+ field,
31
+ locale: locale.toUpperCase()
32
+ })
33
+ };
34
+ }
35
+
36
+ //# sourceMappingURL=validation-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/crud/validation-messages.ts"],"sourcesContent":["import type { EntityDraftMessages } from '@entifix/react-integration';\n\n/** A translate function bound to the `controls` namespace. */\nexport type ControlsTranslate = (\n key: string,\n params?: Record<string, unknown>,\n) => string;\n\n/**\n * The form's validation sentences, in the `controls` catalog's words.\n *\n * The integration package owns the validation *rules* and this shell owns the\n * sentences, because a sideways import between the two T3 packages is an edge\n * the tier register does not declare. `count` doubles as i18next's plural\n * selector, so `minItems` reads \"1 item\" and \"2 items\".\n */\nexport function validationMessagesFor(\n ct: ControlsTranslate,\n): EntityDraftMessages {\n const say =\n (failure: 'required' | 'number' | 'date' | 'option') => (field: string) =>\n ct(`validation.${failure}`, { field });\n return {\n required: say('required'),\n number: say('number'),\n date: say('date'),\n option: say('option'),\n minItems: (field, count) => ct('validation.minItems', { field, count }),\n maxItems: (field, count) => ct('validation.maxItems', { field, count }),\n notBefore: (field, other) => ct('validation.notBefore', { field, other }),\n locale: (field, locale) =>\n ct('validation.locale', { field, locale: locale.toUpperCase() }),\n };\n}\n"],"names":["validationMessagesFor","ct","say","failure","field","required","number","date","option","minItems","count","maxItems","notBefore","other","locale","toUpperCase"],"mappings":"AAQA;;;;;;;CAOC,GACD,OAAO,SAASA,sBACdC,EAAqB;IAErB,MAAMC,MACJ,CAACC,UAAuD,CAACC,QACvDH,GAAG,CAAC,WAAW,EAAEE,SAAS,EAAE;gBAAEC;YAAM;IACxC,OAAO;QACLC,UAAUH,IAAI;QACdI,QAAQJ,IAAI;QACZK,MAAML,IAAI;QACVM,QAAQN,IAAI;QACZO,UAAU,CAACL,OAAOM,QAAUT,GAAG,uBAAuB;gBAAEG;gBAAOM;YAAM;QACrEC,UAAU,CAACP,OAAOM,QAAUT,GAAG,uBAAuB;gBAAEG;gBAAOM;YAAM;QACrEE,WAAW,CAACR,OAAOS,QAAUZ,GAAG,wBAAwB;gBAAEG;gBAAOS;YAAM;QACvEC,QAAQ,CAACV,OAAOU,SACdb,GAAG,qBAAqB;gBAAEG;gBAAOU,QAAQA,OAAOC,WAAW;YAAG;IAClE;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"record-search-source.d.ts","sourceRoot":"","sources":["../../../src/lib/search/record-search-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,CACb,IAAI,EAAE,OAAO,KACV;QAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjE;AAoBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAAE,uGAS9D,wBAAwB,CAAC,OAAO,CAAC,KAAG,kBAiItC,CAAC"}
1
+ {"version":3,"file":"record-search-source.d.ts","sourceRoot":"","sources":["../../../src/lib/search/record-search-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvD,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,CACb,IAAI,EAAE,OAAO,KACV;QAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjE;AAoBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAAE,uGAS9D,wBAAwB,CAAC,OAAO,CAAC,KAAG,kBA2ItC,CAAC"}
@@ -45,6 +45,11 @@ const rejection = (entityName, member, because, fix)=>new EntifixLogicError(`Can
45
45
  // the service rejects the sort and the group degrades whole. Unsorted was the
46
46
  // alternative and is worse — the services apply no default order, so results
47
47
  // would arrive in storage order and shuffle as records are rewritten.
48
+ // A localized label sorts one locale at a time, and a search that fans out
49
+ // across services holds none: the bare member would answer `400`.
50
+ if (label.type === 'localized') {
51
+ throw rejection(entityName, labelProperty, 'the label member is localized, and the search has no locale to sort it in.', 'Label records by a string member.');
52
+ }
48
53
  if (!label.sortable) {
49
54
  throw rejection(entityName, labelProperty, 'the label member is not sortable, so the service would reject the sort.', 'Declare it `sortable` on the entity, or label records by another member.');
50
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/search/record-search-source.ts"],"sourcesContent":["import {\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityLoadRequest,\n envelopeEntityName,\n readEntityPageEnvelope,\n serializeLoadRequestParams,\n} from '@entifix/core';\nimport { Effect, Exit } from 'effect';\n\nimport type { RecordSearchOption } from './record-search.types.js';\n\n/**\n * What a host declares in order to make one entity searchable from the palette\n * (ADR 0040).\n *\n * The search and label members are **declared**, not derived, and that is not an\n * oversight in the metadata: `linkSearchProperty`/`linkLabelProperty` are\n * properties of a *referring* accessor — they say how the owner of a relation\n * looks up its target — and an entity has no member that says \"this is what I am\n * called\". So the host that mounts the screens says it, next to the route those\n * screens live at.\n */\nexport interface RecordSearchSourceConfig<TEntity extends Entity> {\n /** Stable id, unique per host. Defaults to the entity's wire name. */\n readonly key?: string;\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** Where the owning service listens, **without** the `/api` suffix. */\n readonly baseUrl: string;\n /** The `filterable` string member a term is matched against with `like`. */\n readonly searchProperty: string;\n /** The `sortable` member read as a record's label. */\n readonly labelProperty: string;\n /** An optional secondary line — a code, a role. */\n readonly sublabelProperty?: string;\n /** The group heading, as the entity's own `pluralKey`. */\n readonly labelKey: string;\n /** Where selecting a record lands. Locale-free; the caller prefixes it. */\n readonly href: (id: string) => string;\n}\n\n/** A declared source, reduced to what the fan-out actually calls. */\nexport interface RecordSearchSource {\n readonly key: string;\n readonly entity: string;\n readonly labelKey: string;\n /** The upstream URL for one term. */\n readonly url: (term: string, limit: number) => string;\n /** `undefined` when the body is not a readable `entityPage` envelope. */\n readonly read: (\n body: unknown,\n ) => { items: RecordSearchOption[]; total: number } | undefined;\n}\n\nconst descriptorOf = (\n columns: EntityFieldDescriptor[],\n name: string,\n): EntityFieldDescriptor | undefined =>\n columns.find(column => column.name === name);\n\nconst rejection = (\n entityName: string,\n member: string,\n because: string,\n fix: string,\n): EntifixLogicError =>\n new EntifixLogicError(\n `Cannot search ${entityName} by \"${member}\": ${because} ${fix}`,\n undefined,\n { entity: entityName, member },\n );\n\n/**\n * Declare a record search source, validating it against the entity's own\n * metadata **now** rather than on the first keystroke.\n *\n * Every check here guards a failure that is otherwise silent at both ends: the\n * service answers `400`, and a caller renders that as a group it could not\n * reach — which reads as \"there are no products\". `assertSearchable` took the\n * same posture for the relation picker, for the same reason.\n *\n * ⚠️ This throws at **module load**, so a bad declaration fails the whole app at\n * boot rather than just search. That is deliberate — a source silently missing\n * from a palette is indistinguishable from a permission the caller lacks — but\n * the blast radius is wider than the picker's, which fails only its own render.\n */\nexport const defineRecordSearchSource = <TEntity extends Entity>({\n key,\n entityConstructor,\n baseUrl,\n searchProperty,\n labelProperty,\n sublabelProperty,\n labelKey,\n href,\n}: RecordSearchSourceConfig<TEntity>): RecordSearchSource => {\n const entity = envelopeEntityName(entityConstructor);\n const entityName = entityConstructor.name;\n const columns = describeEntityColumns(entityConstructor);\n\n const search = descriptorOf(columns, searchProperty);\n if (search === undefined) {\n throw rejection(\n entityName,\n searchProperty,\n 'the entity declares no such member.',\n 'Name a member the entity actually has.',\n );\n }\n // `filterable` is simultaneously the server-side RSQL allowlist.\n if (!search.filterable) {\n throw rejection(\n entityName,\n searchProperty,\n 'the member is not filterable, so the service would reject the query.',\n 'Declare it `filterable` on the entity, or search another member.',\n );\n }\n // A `like` against anything else reaches `coerceValue`, which rejects a\n // partial term — so an enum member reads as declarable and is permanently\n // broken: every keystroke short of a whole value answers `400`.\n if (search.type !== 'string') {\n throw rejection(\n entityName,\n searchProperty,\n `the member is a ${search.type}, and a partial term is not a valid value for it.`,\n 'Search a string member instead.',\n );\n }\n\n const label = descriptorOf(columns, labelProperty);\n if (label === undefined) {\n throw rejection(\n entityName,\n labelProperty,\n 'the entity declares no such label member.',\n 'Name a member the entity actually has.',\n );\n }\n // The group is sorted by the label, so the flag is load-bearing: without it\n // the service rejects the sort and the group degrades whole. Unsorted was the\n // alternative and is worse — the services apply no default order, so results\n // would arrive in storage order and shuffle as records are rewritten.\n if (!label.sortable) {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is not sortable, so the service would reject the sort.',\n 'Declare it `sortable` on the entity, or label records by another member.',\n );\n }\n\n let sublabel: EntityFieldDescriptor | undefined;\n if (sublabelProperty !== undefined) {\n sublabel = descriptorOf(columns, sublabelProperty);\n if (sublabel === undefined) {\n throw rejection(\n entityName,\n sublabelProperty,\n 'the entity declares no such sublabel member.',\n 'Name a member the entity actually has, or drop `sublabelProperty`.',\n );\n }\n }\n\n const read = (\n body: unknown,\n ): { items: RecordSearchOption[]; total: number } | undefined => {\n const page = Effect.runSyncExit(\n readEntityPageEnvelope(entityConstructor, body),\n );\n if (Exit.isFailure(page)) return undefined;\n\n const items = page.value.items\n .filter(record => (record as Entity).id != null)\n .map(record => {\n const row = record as unknown as Record<string, unknown>;\n const raw = row[labelProperty];\n const id = String((record as Entity).id);\n return {\n id,\n // A blank label is as useless as a missing one, so both fall back to\n // the id — an unnamed record must stay distinguishable from the one\n // above it. Same rule the relation picker's `labelOf` applies.\n label: raw == null || raw === '' ? id : String(raw),\n ...(sublabel !== undefined && row[sublabel.name] != null\n ? { sublabel: String(row[sublabel.name]) }\n : {}),\n entity,\n href: href(id),\n } satisfies RecordSearchOption;\n });\n\n return { items, total: page.value.total };\n };\n\n return {\n key: key ?? entity,\n entity,\n labelKey,\n /**\n * Built through `serializeLoadRequestParams`, never by hand. That is\n * correctness rather than tidiness: `encodeRsqlValue` quotes a term carrying\n * any of `;,()'\"=!<>\\` or whitespace, so `Acme, Inc.` stays one value\n * instead of splitting the expression into two comparisons.\n */\n url: (term, limit) => {\n const request = {\n filtering: [\n {\n property: search.key,\n operator: 'like' as const,\n value: term,\n },\n ],\n sorting: [{ 0: { property: label.key, type: 'asc' as const } }],\n page: 1,\n pageSize: limit,\n } as unknown as EntityLoadRequest;\n\n return `${baseUrl}/api/${entity}?${serializeLoadRequestParams(request).toString()}`;\n },\n read,\n };\n};\n"],"names":["describeEntityColumns","EntifixLogicError","envelopeEntityName","readEntityPageEnvelope","serializeLoadRequestParams","Effect","Exit","descriptorOf","columns","name","find","column","rejection","entityName","member","because","fix","undefined","entity","defineRecordSearchSource","key","entityConstructor","baseUrl","searchProperty","labelProperty","sublabelProperty","labelKey","href","search","filterable","type","label","sortable","sublabel","read","body","page","runSyncExit","isFailure","items","value","filter","record","id","map","row","raw","String","total","url","term","limit","request","filtering","property","operator","sorting","pageSize","toString"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/search/record-search-source.ts"],"sourcesContent":["import {\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityLoadRequest,\n envelopeEntityName,\n readEntityPageEnvelope,\n serializeLoadRequestParams,\n} from '@entifix/core';\nimport { Effect, Exit } from 'effect';\n\nimport type { RecordSearchOption } from './record-search.types.js';\n\n/**\n * What a host declares in order to make one entity searchable from the palette\n * (ADR 0040).\n *\n * The search and label members are **declared**, not derived, and that is not an\n * oversight in the metadata: `linkSearchProperty`/`linkLabelProperty` are\n * properties of a *referring* accessor — they say how the owner of a relation\n * looks up its target — and an entity has no member that says \"this is what I am\n * called\". So the host that mounts the screens says it, next to the route those\n * screens live at.\n */\nexport interface RecordSearchSourceConfig<TEntity extends Entity> {\n /** Stable id, unique per host. Defaults to the entity's wire name. */\n readonly key?: string;\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** Where the owning service listens, **without** the `/api` suffix. */\n readonly baseUrl: string;\n /** The `filterable` string member a term is matched against with `like`. */\n readonly searchProperty: string;\n /** The `sortable` member read as a record's label. */\n readonly labelProperty: string;\n /** An optional secondary line — a code, a role. */\n readonly sublabelProperty?: string;\n /** The group heading, as the entity's own `pluralKey`. */\n readonly labelKey: string;\n /** Where selecting a record lands. Locale-free; the caller prefixes it. */\n readonly href: (id: string) => string;\n}\n\n/** A declared source, reduced to what the fan-out actually calls. */\nexport interface RecordSearchSource {\n readonly key: string;\n readonly entity: string;\n readonly labelKey: string;\n /** The upstream URL for one term. */\n readonly url: (term: string, limit: number) => string;\n /** `undefined` when the body is not a readable `entityPage` envelope. */\n readonly read: (\n body: unknown,\n ) => { items: RecordSearchOption[]; total: number } | undefined;\n}\n\nconst descriptorOf = (\n columns: EntityFieldDescriptor[],\n name: string,\n): EntityFieldDescriptor | undefined =>\n columns.find(column => column.name === name);\n\nconst rejection = (\n entityName: string,\n member: string,\n because: string,\n fix: string,\n): EntifixLogicError =>\n new EntifixLogicError(\n `Cannot search ${entityName} by \"${member}\": ${because} ${fix}`,\n undefined,\n { entity: entityName, member },\n );\n\n/**\n * Declare a record search source, validating it against the entity's own\n * metadata **now** rather than on the first keystroke.\n *\n * Every check here guards a failure that is otherwise silent at both ends: the\n * service answers `400`, and a caller renders that as a group it could not\n * reach — which reads as \"there are no products\". `assertSearchable` took the\n * same posture for the relation picker, for the same reason.\n *\n * ⚠️ This throws at **module load**, so a bad declaration fails the whole app at\n * boot rather than just search. That is deliberate — a source silently missing\n * from a palette is indistinguishable from a permission the caller lacks — but\n * the blast radius is wider than the picker's, which fails only its own render.\n */\nexport const defineRecordSearchSource = <TEntity extends Entity>({\n key,\n entityConstructor,\n baseUrl,\n searchProperty,\n labelProperty,\n sublabelProperty,\n labelKey,\n href,\n}: RecordSearchSourceConfig<TEntity>): RecordSearchSource => {\n const entity = envelopeEntityName(entityConstructor);\n const entityName = entityConstructor.name;\n const columns = describeEntityColumns(entityConstructor);\n\n const search = descriptorOf(columns, searchProperty);\n if (search === undefined) {\n throw rejection(\n entityName,\n searchProperty,\n 'the entity declares no such member.',\n 'Name a member the entity actually has.',\n );\n }\n // `filterable` is simultaneously the server-side RSQL allowlist.\n if (!search.filterable) {\n throw rejection(\n entityName,\n searchProperty,\n 'the member is not filterable, so the service would reject the query.',\n 'Declare it `filterable` on the entity, or search another member.',\n );\n }\n // A `like` against anything else reaches `coerceValue`, which rejects a\n // partial term — so an enum member reads as declarable and is permanently\n // broken: every keystroke short of a whole value answers `400`.\n if (search.type !== 'string') {\n throw rejection(\n entityName,\n searchProperty,\n `the member is a ${search.type}, and a partial term is not a valid value for it.`,\n 'Search a string member instead.',\n );\n }\n\n const label = descriptorOf(columns, labelProperty);\n if (label === undefined) {\n throw rejection(\n entityName,\n labelProperty,\n 'the entity declares no such label member.',\n 'Name a member the entity actually has.',\n );\n }\n // The group is sorted by the label, so the flag is load-bearing: without it\n // the service rejects the sort and the group degrades whole. Unsorted was the\n // alternative and is worse — the services apply no default order, so results\n // would arrive in storage order and shuffle as records are rewritten.\n // A localized label sorts one locale at a time, and a search that fans out\n // across services holds none: the bare member would answer `400`.\n if (label.type === 'localized') {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is localized, and the search has no locale to sort it in.',\n 'Label records by a string member.',\n );\n }\n if (!label.sortable) {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is not sortable, so the service would reject the sort.',\n 'Declare it `sortable` on the entity, or label records by another member.',\n );\n }\n\n let sublabel: EntityFieldDescriptor | undefined;\n if (sublabelProperty !== undefined) {\n sublabel = descriptorOf(columns, sublabelProperty);\n if (sublabel === undefined) {\n throw rejection(\n entityName,\n sublabelProperty,\n 'the entity declares no such sublabel member.',\n 'Name a member the entity actually has, or drop `sublabelProperty`.',\n );\n }\n }\n\n const read = (\n body: unknown,\n ): { items: RecordSearchOption[]; total: number } | undefined => {\n const page = Effect.runSyncExit(\n readEntityPageEnvelope(entityConstructor, body),\n );\n if (Exit.isFailure(page)) return undefined;\n\n const items = page.value.items\n .filter(record => (record as Entity).id != null)\n .map(record => {\n const row = record as unknown as Record<string, unknown>;\n const raw = row[labelProperty];\n const id = String((record as Entity).id);\n return {\n id,\n // A blank label is as useless as a missing one, so both fall back to\n // the id — an unnamed record must stay distinguishable from the one\n // above it. Same rule the relation picker's `labelOf` applies.\n label: raw == null || raw === '' ? id : String(raw),\n ...(sublabel !== undefined && row[sublabel.name] != null\n ? { sublabel: String(row[sublabel.name]) }\n : {}),\n entity,\n href: href(id),\n } satisfies RecordSearchOption;\n });\n\n return { items, total: page.value.total };\n };\n\n return {\n key: key ?? entity,\n entity,\n labelKey,\n /**\n * Built through `serializeLoadRequestParams`, never by hand. That is\n * correctness rather than tidiness: `encodeRsqlValue` quotes a term carrying\n * any of `;,()'\"=!<>\\` or whitespace, so `Acme, Inc.` stays one value\n * instead of splitting the expression into two comparisons.\n */\n url: (term, limit) => {\n const request = {\n filtering: [\n {\n property: search.key,\n operator: 'like' as const,\n value: term,\n },\n ],\n sorting: [{ 0: { property: label.key, type: 'asc' as const } }],\n page: 1,\n pageSize: limit,\n } as unknown as EntityLoadRequest;\n\n return `${baseUrl}/api/${entity}?${serializeLoadRequestParams(request).toString()}`;\n },\n read,\n };\n};\n"],"names":["describeEntityColumns","EntifixLogicError","envelopeEntityName","readEntityPageEnvelope","serializeLoadRequestParams","Effect","Exit","descriptorOf","columns","name","find","column","rejection","entityName","member","because","fix","undefined","entity","defineRecordSearchSource","key","entityConstructor","baseUrl","searchProperty","labelProperty","sublabelProperty","labelKey","href","search","filterable","type","label","sortable","sublabel","read","body","page","runSyncExit","isFailure","items","value","filter","record","id","map","row","raw","String","total","url","term","limit","request","filtering","property","operator","sorting","pageSize","toString"],"mappings":"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"}
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  }
22
22
  }
23
23
  },
24
- "version": "0.2.0",
24
+ "version": "0.3.0",
25
25
  "description": "Next.js application shell: back-office chrome, workspace tabs, CRUD screens derived from entity metadata, and session and proxy routes.",
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
  "./server": {
48
- "@entifix/source": "./src/server.ts",
49
47
  "types": "./dist/server.d.ts",
50
48
  "import": "./dist/server.js",
51
49
  "default": "./dist/server.js"
@@ -59,17 +57,17 @@
59
57
  "react": "19.2.8",
60
58
  "react-dom": "19.2.8",
61
59
  "zustand": "^5.0.15",
62
- "@entifix/testing-unit": "0.2.0"
60
+ "@entifix/testing-unit": "0.3.0"
63
61
  },
64
62
  "dependencies": {
65
63
  "idb": "^8.0.3",
66
- "@entifix/authz": "0.2.0",
67
- "@entifix/react-controls": "0.2.0",
68
- "@entifix/react-integration": "0.2.0",
69
- "@entifix/business": "0.2.0",
70
- "@entifix/transactions": "0.2.0",
71
- "@entifix/i18n": "0.2.0",
72
- "@entifix/core": "0.2.0"
64
+ "@entifix/authz": "0.3.0",
65
+ "@entifix/react-integration": "0.3.0",
66
+ "@entifix/transactions": "0.3.0",
67
+ "@entifix/i18n": "0.3.0",
68
+ "@entifix/business": "0.3.0",
69
+ "@entifix/core": "0.3.0",
70
+ "@entifix/react-controls": "0.3.0"
73
71
  },
74
72
  "files": [
75
73
  "dist",