@entifix/react-integration 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/context/adapters-context/adapters-context.d.ts.map +1 -1
  2. package/dist/context/adapters-context/adapters-context.js +1 -2
  3. package/dist/context/adapters-context/adapters-context.js.map +1 -1
  4. package/dist/hooks/use-data-loading/use-data-loading.d.ts +2 -2
  5. package/dist/hooks/use-data-loading/use-data-loading.d.ts.map +1 -1
  6. package/dist/hooks/use-data-loading/use-data-loading.js.map +1 -1
  7. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts +3 -3
  8. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts.map +1 -1
  9. package/dist/hooks/use-data-loading/use-data-loading.types.js.map +1 -1
  10. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -1
  11. package/dist/hooks/use-entity-form/use-entity-form.helpers.js +2 -2
  12. package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -1
  13. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -1
  14. package/dist/hooks/use-entity-link-source/use-entity-link-source.js +3 -9
  15. package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -1
  16. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts +3 -3
  17. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts.map +1 -1
  18. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js.map +1 -1
  19. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts +2 -2
  20. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts.map +1 -1
  21. package/dist/hooks/use-entity-mutation/use-entity-mutation.js.map +1 -1
  22. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts +4 -4
  23. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts.map +1 -1
  24. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js.map +1 -1
  25. package/dist/hooks/use-entity-record/use-entity-record.d.ts +2 -2
  26. package/dist/hooks/use-entity-record/use-entity-record.d.ts.map +1 -1
  27. package/dist/hooks/use-entity-record/use-entity-record.js.map +1 -1
  28. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts +3 -3
  29. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts.map +1 -1
  30. package/dist/hooks/use-entity-record/use-entity-record.types.js.map +1 -1
  31. package/dist/query/query-provider.d.ts +1 -1
  32. package/dist/query/query-provider.js +1 -1
  33. package/dist/query/query-provider.js.map +1 -1
  34. package/dist/url/url-query.d.ts.map +1 -1
  35. package/dist/url/url-query.js +2 -8
  36. package/dist/url/url-query.js.map +1 -1
  37. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"adapters-context.d.ts","sourceRoot":"","sources":["../../../src/context/adapters-context/adapters-context.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAA6B,MAAM,OAAO,CAAC;AAEhE,wBAAgB,qBAAqB,CAAC,SAAS,wBAE9C;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,aAaxE"}
1
+ {"version":3,"file":"adapters-context.d.ts","sourceRoot":"","sources":["../../../src/context/adapters-context/adapters-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAA6B,MAAM,OAAO,CAAC;AAEhE,wBAAgB,qBAAqB,CAAC,SAAS,wBAE9C;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,aAaxE"}
@@ -1,4 +1,3 @@
1
- import { EntifixBuildError } from '@entifix/core';
2
1
  import { createContext, useContext } from 'react';
3
2
  export function createAdaptersContext() {
4
3
  return /*#__PURE__*/ createContext({});
@@ -11,7 +10,7 @@ export function useAdaptersContext(context) {
11
10
  // for strings, arrays and null, none of which can reach here, and it was the
12
11
  // single import crossing out of entifix.
13
12
  if (Object.keys(contextValue).length === 0) {
14
- throw new EntifixBuildError('Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.');
13
+ throw new Error('Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.');
15
14
  }
16
15
  return contextValue;
17
16
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/context/adapters-context/adapters-context.tsx"],"sourcesContent":["import { EntifixBuildError } from '@entifix/core';\nimport { type Context, createContext, useContext } from 'react';\n\nexport function createAdaptersContext<TAdapters>() {\n return createContext({} as TAdapters);\n}\n\nexport function useAdaptersContext<TAdapters>(context: Context<TAdapters>) {\n const contextValue = useContext(context);\n // The default `createAdaptersContext` hands out is `{}`, so \"nobody mounted\n // a provider\" and \"the object has no keys\" are the same question. Asked\n // directly rather than through a generic `isEmpty`: that helper also answers\n // for strings, arrays and null, none of which can reach here, and it was the\n // single import crossing out of entifix.\n if (Object.keys(contextValue as object).length === 0) {\n throw new EntifixBuildError(\n 'Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.',\n );\n }\n return contextValue;\n}\n"],"names":["EntifixBuildError","createContext","useContext","createAdaptersContext","useAdaptersContext","context","contextValue","Object","keys","length"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,gBAAgB;AAClD,SAAuBC,aAAa,EAAEC,UAAU,QAAQ,QAAQ;AAEhE,OAAO,SAASC;IACd,qBAAOF,cAAc,CAAC;AACxB;AAEA,OAAO,SAASG,mBAA8BC,OAA2B;IACvE,MAAMC,eAAeJ,WAAWG;IAChC,4EAA4E;IAC5E,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yCAAyC;IACzC,IAAIE,OAAOC,IAAI,CAACF,cAAwBG,MAAM,KAAK,GAAG;QACpD,MAAM,IAAIT,kBACR;IAEJ;IACA,OAAOM;AACT"}
1
+ {"version":3,"sources":["../../../src/context/adapters-context/adapters-context.tsx"],"sourcesContent":["import { type Context, createContext, useContext } from 'react';\n\nexport function createAdaptersContext<TAdapters>() {\n return createContext({} as TAdapters);\n}\n\nexport function useAdaptersContext<TAdapters>(context: Context<TAdapters>) {\n const contextValue = useContext(context);\n // The default `createAdaptersContext` hands out is `{}`, so \"nobody mounted\n // a provider\" and \"the object has no keys\" are the same question. Asked\n // directly rather than through a generic `isEmpty`: that helper also answers\n // for strings, arrays and null, none of which can reach here, and it was the\n // single import crossing out of entifix.\n if (Object.keys(contextValue as object).length === 0) {\n throw new Error(\n 'Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.',\n );\n }\n return contextValue;\n}\n"],"names":["createContext","useContext","createAdaptersContext","useAdaptersContext","context","contextValue","Object","keys","length","Error"],"mappings":"AAAA,SAAuBA,aAAa,EAAEC,UAAU,QAAQ,QAAQ;AAEhE,OAAO,SAASC;IACd,qBAAOF,cAAc,CAAC;AACxB;AAEA,OAAO,SAASG,mBAA8BC,OAA2B;IACvE,MAAMC,eAAeJ,WAAWG;IAChC,4EAA4E;IAC5E,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yCAAyC;IACzC,IAAIE,OAAOC,IAAI,CAACF,cAAwBG,MAAM,KAAK,GAAG;QACpD,MAAM,IAAIC,MACR;IAEJ;IACA,OAAOJ;AACT"}
@@ -1,4 +1,4 @@
1
- import { type EntifixError, type Entity, type EntitySorting, type FilterGroup } from '@entifix/core';
1
+ import { type Entity, type EntitySorting, type FilterGroup, type TransitionalFailure } from '@entifix/core';
2
2
  import type { UseDataLoadingOptions } from './use-data-loading.types.js';
3
3
  /**
4
4
  * Runs an entity list use-case and caches the result through TanStack Query.
@@ -15,7 +15,7 @@ export declare function useDataLoading<TEntity extends Entity, TContext>({ uc, c
15
15
  totalItems: number;
16
16
  currentPage: number;
17
17
  pageSize: number;
18
- error: EntifixError | undefined;
18
+ error: TransitionalFailure | undefined;
19
19
  filtering: FilterGroup<TEntity> | undefined;
20
20
  sorting: EntitySorting<TEntity> | undefined;
21
21
  onPageChange: (newPage: number) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"use-data-loading.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EAGX,KAAK,aAAa,EAClB,KAAK,WAAW,EAGjB,MAAM,eAAe,CAAC;AAavB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAiCzE;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAC/D,EAAE,EACF,GAAG,EACH,eAAmC,EACnC,QAAQ,EACR,OAAc,EACd,aAAa,GACd,EAAE,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC;;;;;;;;;4BA0GE,MAAM;oCAIE,MAAM;mCAMP,WAAW,CAAC,OAAO,CAAC;+BAIxB,aAAa,CAAC,OAAO,CAAC;EAkBrE"}
1
+ {"version":3,"file":"use-data-loading.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAGX,KAAK,aAAa,EAClB,KAAK,WAAW,EAGhB,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AAavB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAiCzE;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAC/D,EAAE,EACF,GAAG,EACH,eAAmC,EACnC,QAAQ,EACR,OAAc,EACd,aAAa,GACd,EAAE,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC;;;;;;;;;4BA0GE,MAAM;oCAIE,MAAM;mCAMP,WAAW,CAAC,OAAO,CAAC;+BAIxB,aAAa,CAAC,OAAO,CAAC;EAkBrE"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n type EntifixError,\n type Entity,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n type FilterGroup,\n serializeRsql,\n serializeSort,\n} from '@entifix/core';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useReducer,\n useRef,\n} from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport type { UseDataLoadingOptions } from './use-data-loading.types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\n\n/**\n * The request the caller controls — page, size, and the *applied* filter/sort.\n * The response (items, loading, error) is owned by the query cache, not here.\n */\ninterface RequestState<T extends Entity> {\n currentPage: number;\n pageSize: number;\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\ntype RequestAction<T extends Entity> = { set: Partial<RequestState<T>> };\n\nfunction requestReducer<T extends Entity>(\n state: RequestState<T>,\n action: RequestAction<T>,\n): RequestState<T> {\n return { ...state, ...action.set };\n}\n\nfunction initialRequest<T extends Entity>(pageSize: number): RequestState<T> {\n return {\n currentPage: 1,\n pageSize,\n filtering: undefined,\n sorting: undefined,\n };\n}\n\n/**\n * Runs an entity list use-case and caches the result through TanStack Query.\n *\n * The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the\n * *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations\n * and reactive events invalidate the list. The public shape is unchanged: the\n * `uc`/`ctx` come in the same way and the Effect runs against the same provided\n * Tags, so callers and organisms are untouched.\n */\nexport function useDataLoading<TEntity extends Entity, TContext>({\n uc,\n ctx,\n initialPageSize = DEFAULT_PAGE_SIZE,\n queryKey,\n enabled = true,\n baseFiltering,\n}: UseDataLoadingOptions<TEntity, TContext>) {\n const [request, dispatch] = useReducer(\n requestReducer as (\n state: RequestState<TEntity>,\n action: RequestAction<TEntity>,\n ) => RequestState<TEntity>,\n initialPageSize,\n initialRequest<TEntity>,\n );\n\n // Callers rebuild `uc`/`ctx` inline every render, so hold the latest in refs\n // and keep them out of the query key — keying on their identity would refetch\n // on every render. Written in an effect (refs may not be set during render);\n // effects fire in order, so the query below always reads the latest.\n const ucRef = useRef(uc);\n const ctxRef = useRef(ctx);\n useEffect(() => {\n ucRef.current = uc;\n ctxRef.current = ctx;\n });\n\n // What actually goes on the wire: the caller's standing restriction ANDed with\n // whatever the user applied. Kept separate from `request.filtering` because the\n // restriction is not the user's filter — the panel must keep showing only what\n // they set, while the request keeps carrying both.\n const effectiveFiltering = useMemo(\n () => combineFilterGroups<TEntity>(baseFiltering, request.filtering),\n [baseFiltering, request.filtering],\n );\n\n // The applied filter/sort are rebuilt each render, so their *serialized* form\n // — the same codec the request needs anyway — is what keys the query: a stable\n // string that changes exactly when the query does.\n const rsql = useMemo(\n () => serializeRsql(effectiveFiltering ? [effectiveFiltering] : undefined),\n [effectiveFiltering],\n );\n const sort = useMemo(\n () => serializeSort(request.sorting ? [request.sorting] : undefined),\n [request.sorting],\n );\n\n const loadRequest = useMemo<EntityLoadRequest<TEntity>>(() => {\n const built: EntityLoadRequest<TEntity> = {\n page: request.currentPage,\n pageSize: request.pageSize,\n };\n // An empty group serializes to nothing; omit it rather than send a filter\n // that matches everything.\n if (rsql !== '' && effectiveFiltering) {\n built.filtering = [effectiveFiltering];\n }\n if (sort !== '' && request.sorting) {\n built.sorting = [request.sorting];\n }\n return built;\n }, [\n request.currentPage,\n request.pageSize,\n effectiveFiltering,\n request.sorting,\n rsql,\n sort,\n ]);\n\n // Falls back to a per-instance id so an un-scoped list never collides with\n // another in the shared cache — correct, just unshared (see the type doc).\n const instanceId = useId();\n const scope = queryKey ?? [instanceId];\n\n const query = useQuery<EntityPage<TEntity>, EntifixError>({\n enabled,\n /**\n * Page, size, filter and sort are all part of the key, so every one of them\n * is a *different* query — and without this, `data` goes undefined the\n * instant any of them changes. The table would then see `items.length === 0`\n * with `isFetching` true, read it as a first load, and blank populated rows\n * to skeleton on every pagination click: the grey flash the loading contract\n * exists to prevent. Keeping the previous page is what makes \"skeleton only\n * on first load\" true rather than merely intended.\n */\n placeholderData: keepPreviousData,\n queryKey: [\n ...scope,\n 'load',\n request.currentPage,\n request.pageSize,\n rsql,\n sort,\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n ucRef.current,\n ctxRef.current.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 loadRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const onPageChange = useCallback((newPage: number) => {\n dispatch({ set: { currentPage: newPage } });\n }, []);\n\n const onPageSizeChange = useCallback((newPageSize: number) => {\n dispatch({ set: { pageSize: newPageSize, currentPage: 1 } });\n }, []);\n\n // Narrowing the result set invalidates the page number: page 4 of the old\n // result is very likely past the end of the new one.\n const onFilteringChange = useCallback((filtering: FilterGroup<TEntity>) => {\n dispatch({ set: { filtering, currentPage: 1 } });\n }, []);\n\n const onSortingChange = useCallback((sorting: EntitySorting<TEntity>) => {\n dispatch({ set: { sorting, currentPage: 1 } });\n }, []);\n\n return {\n isLoading: query.isFetching,\n items: query.data?.items ?? ([] as Array<TEntity>),\n totalItems: query.data?.total ?? 0,\n currentPage: request.currentPage,\n pageSize: request.pageSize,\n error: query.error ?? undefined,\n filtering: request.filtering,\n sorting: request.sorting,\n onPageChange,\n onPageSizeChange,\n onFilteringChange,\n onSortingChange,\n };\n}\n"],"names":["EntityLoadRequestTag","serializeRsql","serializeSort","keepPreviousData","useQuery","Context","Effect","useCallback","useEffect","useId","useMemo","useReducer","useRef","combineFilterGroups","DEFAULT_PAGE_SIZE","requestReducer","state","action","set","initialRequest","pageSize","currentPage","filtering","undefined","sorting","useDataLoading","uc","ctx","initialPageSize","queryKey","enabled","baseFiltering","query","request","dispatch","ucRef","ctxRef","current","effectiveFiltering","rsql","sort","loadRequest","built","page","instanceId","scope","placeholderData","queryFn","runPromise","provide","pipe","add","onPageChange","newPage","onPageSizeChange","newPageSize","onFilteringChange","onSortingChange","isLoading","isFetching","items","data","totalItems","total","error"],"mappings":";AAAA,SAASA,oBAAoB,QAAQ,oBAAoB;AACzD,SAOEC,aAAa,EACbC,aAAa,QACR,gBAAgB;AACvB,SAASC,gBAAgB,EAAEC,QAAQ,QAAQ,wBAAwB;AACnE,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SACEC,WAAW,EACXC,SAAS,EACTC,KAAK,EACLC,OAAO,EACPC,UAAU,EACVC,MAAM,QACD,QAAQ;AAEf,SAASC,mBAAmB,QAAQ,mCAAmC;AAGvE,MAAMC,oBAAoB;AAe1B,SAASC,eACPC,KAAsB,EACtBC,MAAwB;IAExB,OAAO,aAAKD,OAAUC,OAAOC,GAAG;AAClC;AAEA,SAASC,eAAiCC,QAAgB;IACxD,OAAO;QACLC,aAAa;QACbD;QACAE,WAAWC;QACXC,SAASD;IACX;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,eAAiD,EAC/DC,EAAE,EACFC,GAAG,EACHC,kBAAkBd,iBAAiB,EACnCe,QAAQ,EACRC,UAAU,IAAI,EACdC,aAAa,EAC4B;qBAkIhCC;QAJAA,aACKA;IA9Hd,MAAM,CAACC,SAASC,SAAS,GAAGvB,WAC1BI,gBAIAa,iBACAT;IAGF,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,qEAAqE;IACrE,MAAMgB,QAAQvB,OAAOc;IACrB,MAAMU,SAASxB,OAAOe;IACtBnB,UAAU;QACR2B,MAAME,OAAO,GAAGX;QAChBU,OAAOC,OAAO,GAAGV;IACnB;IAEA,+EAA+E;IAC/E,gFAAgF;IAChF,+EAA+E;IAC/E,mDAAmD;IACnD,MAAMW,qBAAqB5B,QACzB,IAAMG,oBAA6BkB,eAAeE,QAAQX,SAAS,GACnE;QAACS;QAAeE,QAAQX,SAAS;KAAC;IAGpC,8EAA8E;IAC9E,+EAA+E;IAC/E,mDAAmD;IACnD,MAAMiB,OAAO7B,QACX,IAAMT,cAAcqC,qBAAqB;YAACA;SAAmB,GAAGf,YAChE;QAACe;KAAmB;IAEtB,MAAME,OAAO9B,QACX,IAAMR,cAAc+B,QAAQT,OAAO,GAAG;YAACS,QAAQT,OAAO;SAAC,GAAGD,YAC1D;QAACU,QAAQT,OAAO;KAAC;IAGnB,MAAMiB,cAAc/B,QAAoC;QACtD,MAAMgC,QAAoC;YACxCC,MAAMV,QAAQZ,WAAW;YACzBD,UAAUa,QAAQb,QAAQ;QAC5B;QACA,0EAA0E;QAC1E,2BAA2B;QAC3B,IAAImB,SAAS,MAAMD,oBAAoB;YACrCI,MAAMpB,SAAS,GAAG;gBAACgB;aAAmB;QACxC;QACA,IAAIE,SAAS,MAAMP,QAAQT,OAAO,EAAE;YAClCkB,MAAMlB,OAAO,GAAG;gBAACS,QAAQT,OAAO;aAAC;QACnC;QACA,OAAOkB;IACT,GAAG;QACDT,QAAQZ,WAAW;QACnBY,QAAQb,QAAQ;QAChBkB;QACAL,QAAQT,OAAO;QACfe;QACAC;KACD;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAMI,aAAanC;IACnB,MAAMoC,QAAQhB,mBAAAA,WAAY;QAACe;KAAW;IAEtC,MAAMZ,QAAQ5B,SAA4C;QACxD0B;QACA;;;;;;;;KAQC,GACDgB,iBAAiB3C;QACjB0B,UAAU;eACLgB;YACH;YACAZ,QAAQZ,WAAW;YACnBY,QAAQb,QAAQ;YAChBmB;YACAC;SACD;QACDO,SAAS,IACPzC,OAAO0C,UAAU,CACf1C,OAAO2C,OAAO,CACZd,MAAME,OAAO,EACbD,OAAOC,OAAO,CAACa,IAAI,CACjB7C,QAAQ8C,GAAG,CACTnD,sBACA,kEAAkE;YAClE,4DAA4D;YAC5DyC;IAKZ;IAEA,MAAMW,eAAe7C,YAAY,CAAC8C;QAChCnB,SAAS;YAAEhB,KAAK;gBAAEG,aAAagC;YAAQ;QAAE;IAC3C,GAAG,EAAE;IAEL,MAAMC,mBAAmB/C,YAAY,CAACgD;QACpCrB,SAAS;YAAEhB,KAAK;gBAAEE,UAAUmC;gBAAalC,aAAa;YAAE;QAAE;IAC5D,GAAG,EAAE;IAEL,0EAA0E;IAC1E,qDAAqD;IACrD,MAAMmC,oBAAoBjD,YAAY,CAACe;QACrCY,SAAS;YAAEhB,KAAK;gBAAEI;gBAAWD,aAAa;YAAE;QAAE;IAChD,GAAG,EAAE;IAEL,MAAMoC,kBAAkBlD,YAAY,CAACiB;QACnCU,SAAS;YAAEhB,KAAK;gBAAEM;gBAASH,aAAa;YAAE;QAAE;IAC9C,GAAG,EAAE;IAEL,OAAO;QACLqC,WAAW1B,MAAM2B,UAAU;QAC3BC,KAAK,WAAE5B,cAAAA,MAAM6B,IAAI,qBAAV7B,YAAY4B,KAAK,mBAAK,EAAE;QAC/BE,UAAU,YAAE9B,eAAAA,MAAM6B,IAAI,qBAAV7B,aAAY+B,KAAK,oBAAI;QACjC1C,aAAaY,QAAQZ,WAAW;QAChCD,UAAUa,QAAQb,QAAQ;QAC1B4C,KAAK,GAAEhC,eAAAA,MAAMgC,KAAK,YAAXhC,eAAeT;QACtBD,WAAWW,QAAQX,SAAS;QAC5BE,SAASS,QAAQT,OAAO;QACxB4B;QACAE;QACAE;QACAC;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n type Entity,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n type FilterGroup,\n serializeRsql,\n serializeSort,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useReducer,\n useRef,\n} from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport type { UseDataLoadingOptions } from './use-data-loading.types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\n\n/**\n * The request the caller controls — page, size, and the *applied* filter/sort.\n * The response (items, loading, error) is owned by the query cache, not here.\n */\ninterface RequestState<T extends Entity> {\n currentPage: number;\n pageSize: number;\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\ntype RequestAction<T extends Entity> = { set: Partial<RequestState<T>> };\n\nfunction requestReducer<T extends Entity>(\n state: RequestState<T>,\n action: RequestAction<T>,\n): RequestState<T> {\n return { ...state, ...action.set };\n}\n\nfunction initialRequest<T extends Entity>(pageSize: number): RequestState<T> {\n return {\n currentPage: 1,\n pageSize,\n filtering: undefined,\n sorting: undefined,\n };\n}\n\n/**\n * Runs an entity list use-case and caches the result through TanStack Query.\n *\n * The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the\n * *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations\n * and reactive events invalidate the list. The public shape is unchanged: the\n * `uc`/`ctx` come in the same way and the Effect runs against the same provided\n * Tags, so callers and organisms are untouched.\n */\nexport function useDataLoading<TEntity extends Entity, TContext>({\n uc,\n ctx,\n initialPageSize = DEFAULT_PAGE_SIZE,\n queryKey,\n enabled = true,\n baseFiltering,\n}: UseDataLoadingOptions<TEntity, TContext>) {\n const [request, dispatch] = useReducer(\n requestReducer as (\n state: RequestState<TEntity>,\n action: RequestAction<TEntity>,\n ) => RequestState<TEntity>,\n initialPageSize,\n initialRequest<TEntity>,\n );\n\n // Callers rebuild `uc`/`ctx` inline every render, so hold the latest in refs\n // and keep them out of the query key — keying on their identity would refetch\n // on every render. Written in an effect (refs may not be set during render);\n // effects fire in order, so the query below always reads the latest.\n const ucRef = useRef(uc);\n const ctxRef = useRef(ctx);\n useEffect(() => {\n ucRef.current = uc;\n ctxRef.current = ctx;\n });\n\n // What actually goes on the wire: the caller's standing restriction ANDed with\n // whatever the user applied. Kept separate from `request.filtering` because the\n // restriction is not the user's filter — the panel must keep showing only what\n // they set, while the request keeps carrying both.\n const effectiveFiltering = useMemo(\n () => combineFilterGroups<TEntity>(baseFiltering, request.filtering),\n [baseFiltering, request.filtering],\n );\n\n // The applied filter/sort are rebuilt each render, so their *serialized* form\n // — the same codec the request needs anyway — is what keys the query: a stable\n // string that changes exactly when the query does.\n const rsql = useMemo(\n () => serializeRsql(effectiveFiltering ? [effectiveFiltering] : undefined),\n [effectiveFiltering],\n );\n const sort = useMemo(\n () => serializeSort(request.sorting ? [request.sorting] : undefined),\n [request.sorting],\n );\n\n const loadRequest = useMemo<EntityLoadRequest<TEntity>>(() => {\n const built: EntityLoadRequest<TEntity> = {\n page: request.currentPage,\n pageSize: request.pageSize,\n };\n // An empty group serializes to nothing; omit it rather than send a filter\n // that matches everything.\n if (rsql !== '' && effectiveFiltering) {\n built.filtering = [effectiveFiltering];\n }\n if (sort !== '' && request.sorting) {\n built.sorting = [request.sorting];\n }\n return built;\n }, [\n request.currentPage,\n request.pageSize,\n effectiveFiltering,\n request.sorting,\n rsql,\n sort,\n ]);\n\n // Falls back to a per-instance id so an un-scoped list never collides with\n // another in the shared cache — correct, just unshared (see the type doc).\n const instanceId = useId();\n const scope = queryKey ?? [instanceId];\n\n const query = useQuery<EntityPage<TEntity>, TransitionalFailure>({\n enabled,\n /**\n * Page, size, filter and sort are all part of the key, so every one of them\n * is a *different* query — and without this, `data` goes undefined the\n * instant any of them changes. The table would then see `items.length === 0`\n * with `isFetching` true, read it as a first load, and blank populated rows\n * to skeleton on every pagination click: the grey flash the loading contract\n * exists to prevent. Keeping the previous page is what makes \"skeleton only\n * on first load\" true rather than merely intended.\n */\n placeholderData: keepPreviousData,\n queryKey: [\n ...scope,\n 'load',\n request.currentPage,\n request.pageSize,\n rsql,\n sort,\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n ucRef.current,\n ctxRef.current.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 loadRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const onPageChange = useCallback((newPage: number) => {\n dispatch({ set: { currentPage: newPage } });\n }, []);\n\n const onPageSizeChange = useCallback((newPageSize: number) => {\n dispatch({ set: { pageSize: newPageSize, currentPage: 1 } });\n }, []);\n\n // Narrowing the result set invalidates the page number: page 4 of the old\n // result is very likely past the end of the new one.\n const onFilteringChange = useCallback((filtering: FilterGroup<TEntity>) => {\n dispatch({ set: { filtering, currentPage: 1 } });\n }, []);\n\n const onSortingChange = useCallback((sorting: EntitySorting<TEntity>) => {\n dispatch({ set: { sorting, currentPage: 1 } });\n }, []);\n\n return {\n isLoading: query.isFetching,\n items: query.data?.items ?? ([] as Array<TEntity>),\n totalItems: query.data?.total ?? 0,\n currentPage: request.currentPage,\n pageSize: request.pageSize,\n error: query.error ?? undefined,\n filtering: request.filtering,\n sorting: request.sorting,\n onPageChange,\n onPageSizeChange,\n onFilteringChange,\n onSortingChange,\n };\n}\n"],"names":["EntityLoadRequestTag","serializeRsql","serializeSort","keepPreviousData","useQuery","Context","Effect","useCallback","useEffect","useId","useMemo","useReducer","useRef","combineFilterGroups","DEFAULT_PAGE_SIZE","requestReducer","state","action","set","initialRequest","pageSize","currentPage","filtering","undefined","sorting","useDataLoading","uc","ctx","initialPageSize","queryKey","enabled","baseFiltering","query","request","dispatch","ucRef","ctxRef","current","effectiveFiltering","rsql","sort","loadRequest","built","page","instanceId","scope","placeholderData","queryFn","runPromise","provide","pipe","add","onPageChange","newPage","onPageSizeChange","newPageSize","onFilteringChange","onSortingChange","isLoading","isFetching","items","data","totalItems","total","error"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { EntityLoadRequestTag } from '@entifix/business';
2
- import { EntifixError, Entity, EntityPage, EntitySorting, FilterGroup } from '@entifix/core';
2
+ import { Entity, EntityPage, EntitySorting, FilterGroup, type TransitionalFailure } from '@entifix/core';
3
3
  import { Context, Effect } from 'effect';
4
4
  export interface UseDataLoadingState<T extends Entity> {
5
5
  isLoading: boolean;
@@ -7,7 +7,7 @@ export interface UseDataLoadingState<T extends Entity> {
7
7
  totalItems: number;
8
8
  currentPage: number;
9
9
  pageSize: number;
10
- error: EntifixError | undefined;
10
+ error: TransitionalFailure | undefined;
11
11
  /**
12
12
  * The filtering and sorting currently *applied* — not a draft. The controls
13
13
  * commit through `onFilteringChange`/`onSortingChange`, so every value here
@@ -20,7 +20,7 @@ export type UseDataLoadingAction<T extends Entity> = {
20
20
  set: Partial<UseDataLoadingState<T>>;
21
21
  };
22
22
  export interface UseDataLoadingOptions<TEntity extends Entity, TContext> {
23
- uc: Effect.Effect<EntityPage<TEntity>, EntifixError, EntityLoadRequestTag | TContext>;
23
+ uc: Effect.Effect<EntityPage<TEntity>, TransitionalFailure, EntityLoadRequestTag | TContext>;
24
24
  ctx: Context.Context<TContext>;
25
25
  /**
26
26
  * Page size for the first load; defaults to 10. Callers that need the whole
@@ -1 +1 @@
1
- {"version":3,"file":"use-data-loading.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,aAAa,EACb,WAAW,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,GAAG,SAAS,CAAC;IAChC;;;;OAIG;IACH,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,MAAM,IAAI;IACnD,GAAG,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACrE,EAAE,EAAE,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,EACZ,oBAAoB,GAAG,QAAQ,CAChC,CAAC;IACF,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"use-data-loading.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,MAAM,EACN,UAAU,EACV,aAAa,EACb,WAAW,EACX,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,MAAM,IAAI;IACnD,GAAG,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACrE,EAAE,EAAE,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,OAAO,CAAC,EACnB,mBAAmB,EACnB,oBAAoB,GAAG,QAAQ,CAChC,CAAC;IACF,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CACtC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n EntifixError,\n Entity,\n EntityPage,\n EntitySorting,\n FilterGroup,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseDataLoadingState<T extends Entity> {\n isLoading: boolean;\n items: Array<T>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n error: EntifixError | undefined;\n /**\n * The filtering and sorting currently *applied* — not a draft. The controls\n * commit through `onFilteringChange`/`onSortingChange`, so every value here\n * has already been requested from the server.\n */\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\nexport type UseDataLoadingAction<T extends Entity> = {\n set: Partial<UseDataLoadingState<T>>;\n};\n\nexport interface UseDataLoadingOptions<TEntity extends Entity, TContext> {\n uc: Effect.Effect<\n EntityPage<TEntity>,\n EntifixError,\n EntityLoadRequestTag | TContext\n >;\n ctx: Context.Context<TContext>;\n /**\n * Page size for the first load; defaults to 10. Callers that need the whole\n * set rather than a page — a relation picker, say — raise this.\n */\n initialPageSize?: number;\n /**\n * The stable prefix that scopes this list in the shared query cache — pass\n * {@link entityQueryScope}`(Ctor)` so refetches, optimistic writes, and\n * reactive invalidation of the same entity all address one cache entry.\n * Omit it and the hook falls back to a per-instance id: correct in isolation\n * (no cross-list collisions) but unshared, so nothing outside this hook can\n * invalidate it.\n */\n queryKey?: ReadonlyArray<unknown>;\n /**\n * `false` holds the request back without unmounting the hook — what a picker's\n * browse dialog needs, since a list nobody has opened yet must not be fetched.\n * Defaults to `true`, so every existing caller loads on mount as before.\n */\n enabled?: boolean;\n /**\n * A standing restriction ANDed into every request and never shown in the filter\n * panel — \"these are the rows this caller may see at all\", as opposed to the\n * filter the user is editing. A link picker narrowing what may be assigned uses\n * it; so would a listing scoped to one tenant.\n */\n baseFiltering?: FilterGroup<TEntity>;\n}\n"],"names":[],"mappings":"AA8BA,WAkCC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n Entity,\n EntityPage,\n EntitySorting,\n FilterGroup,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseDataLoadingState<T extends Entity> {\n isLoading: boolean;\n items: Array<T>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n error: TransitionalFailure | undefined;\n /**\n * The filtering and sorting currently *applied* — not a draft. The controls\n * commit through `onFilteringChange`/`onSortingChange`, so every value here\n * has already been requested from the server.\n */\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\nexport type UseDataLoadingAction<T extends Entity> = {\n set: Partial<UseDataLoadingState<T>>;\n};\n\nexport interface UseDataLoadingOptions<TEntity extends Entity, TContext> {\n uc: Effect.Effect<\n EntityPage<TEntity>,\n TransitionalFailure,\n EntityLoadRequestTag | TContext\n >;\n ctx: Context.Context<TContext>;\n /**\n * Page size for the first load; defaults to 10. Callers that need the whole\n * set rather than a page — a relation picker, say — raise this.\n */\n initialPageSize?: number;\n /**\n * The stable prefix that scopes this list in the shared query cache — pass\n * {@link entityQueryScope}`(Ctor)` so refetches, optimistic writes, and\n * reactive invalidation of the same entity all address one cache entry.\n * Omit it and the hook falls back to a per-instance id: correct in isolation\n * (no cross-list collisions) but unshared, so nothing outside this hook can\n * invalidate it.\n */\n queryKey?: ReadonlyArray<unknown>;\n /**\n * `false` holds the request back without unmounting the hook — what a picker's\n * browse dialog needs, since a list nobody has opened yet must not be fetched.\n * Defaults to `true`, so every existing caller loads on mount as before.\n */\n enabled?: boolean;\n /**\n * A standing restriction ANDed into every request and never shown in the filter\n * panel — \"these are the rows this caller may see at all\", as opposed to the\n * filter the user is editing. A link picker narrowing what may be assigned uses\n * it; so would a listing scoped to one tenant.\n */\n baseFiltering?: FilterGroup<TEntity>;\n}\n"],"names":[],"mappings":"AA8BA,WAkCC"}
@@ -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":"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"}
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, isLocalizedText, isRowDraftArray, joinFieldPath, locales, readDraftString, readLocalizedDraft, readRowDrafts, ROW_KEY, rowFieldPath, seededRowKey } from '@entifix/core';
2
+ import { describeChildColumns, editableChildColumns, 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
@@ -374,7 +374,7 @@ const labelOf = (descriptor)=>descriptor.label;
374
374
  */ export function readSchemaIssues(schema, values, translateIssue) {
375
375
  const result = schema['~standard'].validate(values);
376
376
  if (result instanceof Promise) {
377
- throw new EntifixLogicError('useEntityForm cannot run an async schema: its validation pass is ' + 'synchronous. Move the asynchronous rule into the `validate` option.');
377
+ throw new Error('useEntityForm cannot run an async schema: its validation pass is ' + 'synchronous. Move the asynchronous rule into the `validate` option.');
378
378
  }
379
379
  if (!result.issues) return {
380
380
  fields: {}
@@ -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 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":"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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 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 Error(\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","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","Error","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":"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"}
@@ -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;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"}
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,EAEL,KAAK,MAAM,EACX,KAAK,gBAAgB,EAItB,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,mCAAmC,CAAC;AAuC3C;;;;;;;;;;;;;;;;;;;;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"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';
4
- import { describeEntityColumns, EntifixLogicError } from '@entifix/core';
4
+ import { describeEntityColumns } from '@entifix/core';
5
5
  import { useQuery } from '@tanstack/react-query';
6
6
  import { Context, Effect } from 'effect';
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -22,16 +22,10 @@ const DEFAULT_DEBOUNCE_MS = 250;
22
22
  if ((target == null ? void 0 : target.type) === 'localized') {
23
23
  // Queried one locale at a time, and this hook holds no locale: the bare
24
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
- });
25
+ throw new Error(`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`.');
29
26
  }
30
27
  if ((target == null ? void 0 : target.filterable) === true) return;
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, {
32
- entity: config.entityConstructor.name,
33
- searchProperty
34
- });
28
+ throw new Error(`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`.');
35
29
  }
36
30
  /**
37
31
  * Produces the {@link EntityLinkSource} a link editor runs on: the quick
@@ -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?.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":"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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 Entity,\n type EntityLinkSource,\n type EntityLoadRequest,\n type EntityPage,\n type TransitionalFailure,\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 Error(\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 );\n }\n if (target?.filterable === true) return;\n\n throw new Error(\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 );\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>, TransitionalFailure>({\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, TransitionalFailure>({\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","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","Error","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","undefined","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","entity","isLoading","isFetching","quick","options","items","error","open","close"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';
2
- import type { EntifixError, Entity, EntityConstructor, EntityFieldDescriptor, EntityId, EntityPage, FilterGroup } from '@entifix/core';
2
+ import type { Entity, EntityConstructor, EntityFieldDescriptor, EntityId, EntityPage, FilterGroup, TransitionalFailure } from '@entifix/core';
3
3
  import type { Context, Effect } from 'effect';
4
4
  /**
5
5
  * Where a picker's targets come from, and what may be picked.
@@ -13,13 +13,13 @@ import type { Context, Effect } from 'effect';
13
13
  export interface EntityLinkSourceConfig<TTarget extends Entity, TContext> {
14
14
  entityConstructor: EntityConstructor<TTarget>;
15
15
  /** Lists the targets. Supplies {@link EntityLoadRequestTag} per request. */
16
- loadUc: Effect.Effect<EntityPage<TTarget>, EntifixError, EntityLoadRequestTag | TContext>;
16
+ loadUc: Effect.Effect<EntityPage<TTarget>, TransitionalFailure, EntityLoadRequestTag | TContext>;
17
17
  /**
18
18
  * Loads one target by id — how a draft restored from storage turns its held
19
19
  * foreign key back into a name. Omit it and the input falls back to showing
20
20
  * the bare key.
21
21
  */
22
- getUc?: Effect.Effect<TTarget, EntifixError, EntityIdTag | TContext>;
22
+ getUc?: Effect.Effect<TTarget, TransitionalFailure, EntityIdTag | TContext>;
23
23
  ctx: Context.Context<TContext>;
24
24
  /**
25
25
  * ANDed into every quick and browse request, so a caller can narrow the
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-link-source.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,iBAAiB,EACjB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACtE,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,CACnB,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,EACZ,oBAAoB,GAAG,QAAQ,CAChC,CAAC;IACF;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IACrE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACrC,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA0B,CAAC,OAAO,SAAS,MAAM;IAChE;;;OAGG;IACH,UAAU,EAAE,qBAAqB,CAAC;IAClC,iDAAiD;IACjD,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B"}
1
+ {"version":3,"file":"use-entity-link-source.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,EACX,mBAAmB,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACtE,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,CACnB,UAAU,CAAC,OAAO,CAAC,EACnB,mBAAmB,EACnB,oBAAoB,GAAG,QAAQ,CAChC,CAAC;IACF;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IAC5E,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACrC,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA0B,CAAC,OAAO,SAAS,MAAM;IAChE;;;OAGG;IACH,UAAU,EAAE,qBAAqB,CAAC;IAClC,iDAAiD;IACjD,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.types.ts"],"sourcesContent":["import type { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport type {\n EntifixError,\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityPage,\n FilterGroup,\n} from '@entifix/core';\nimport type { Context, Effect } from 'effect';\n\n/**\n * Where a picker's targets come from, and what may be picked.\n *\n * The use-cases are the parametrization seam: a domain that must restrict what is\n * assignable supplies its own list use-case (only active brands, only leaf\n * categories) instead of the generic `loadUCFactory`, and the picker never learns\n * the rule. {@link baseFiltering} is the lighter version of the same idea for a\n * restriction that needs no use-case of its own.\n */\nexport interface EntityLinkSourceConfig<TTarget extends Entity, TContext> {\n entityConstructor: EntityConstructor<TTarget>;\n /** Lists the targets. Supplies {@link EntityLoadRequestTag} per request. */\n loadUc: Effect.Effect<\n EntityPage<TTarget>,\n EntifixError,\n EntityLoadRequestTag | TContext\n >;\n /**\n * Loads one target by id — how a draft restored from storage turns its held\n * foreign key back into a name. Omit it and the input falls back to showing\n * the bare key.\n */\n getUc?: Effect.Effect<TTarget, EntifixError, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n /**\n * ANDed into every quick and browse request, so a caller can narrow the\n * selectable set without writing a use-case.\n */\n baseFiltering?: FilterGroup<TTarget>;\n /** How many suggestions the quick search asks for. Defaults to 10. */\n quickPageSize?: number;\n /** Milliseconds of quiet before a typed term is sent. Defaults to 250. */\n debounceMs?: number;\n}\n\nexport interface UseEntityLinkSourceOptions<TTarget extends Entity> {\n /**\n * The relation being edited. Its `linkSearchProperty` is what the quick search\n * filters on and its `linkLabelProperty` is what a target reads as.\n */\n descriptor: EntityFieldDescriptor;\n /** The foreign key the draft currently holds. */\n selectedId?: EntityId;\n /**\n * The instance behind {@link selectedId} when the caller already has it — the\n * one the user just picked, or the one a loaded record carried. Present, it\n * spares the label request entirely.\n */\n selectedEntity?: TTarget;\n}\n"],"names":[],"mappings":"AA+CA,WAcC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.types.ts"],"sourcesContent":["import type { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport type {\n Entity,\n EntityConstructor,\n EntityFieldDescriptor,\n EntityId,\n EntityPage,\n FilterGroup,\n TransitionalFailure,\n} from '@entifix/core';\nimport type { Context, Effect } from 'effect';\n\n/**\n * Where a picker's targets come from, and what may be picked.\n *\n * The use-cases are the parametrization seam: a domain that must restrict what is\n * assignable supplies its own list use-case (only active brands, only leaf\n * categories) instead of the generic `loadUCFactory`, and the picker never learns\n * the rule. {@link baseFiltering} is the lighter version of the same idea for a\n * restriction that needs no use-case of its own.\n */\nexport interface EntityLinkSourceConfig<TTarget extends Entity, TContext> {\n entityConstructor: EntityConstructor<TTarget>;\n /** Lists the targets. Supplies {@link EntityLoadRequestTag} per request. */\n loadUc: Effect.Effect<\n EntityPage<TTarget>,\n TransitionalFailure,\n EntityLoadRequestTag | TContext\n >;\n /**\n * Loads one target by id — how a draft restored from storage turns its held\n * foreign key back into a name. Omit it and the input falls back to showing\n * the bare key.\n */\n getUc?: Effect.Effect<TTarget, TransitionalFailure, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n /**\n * ANDed into every quick and browse request, so a caller can narrow the\n * selectable set without writing a use-case.\n */\n baseFiltering?: FilterGroup<TTarget>;\n /** How many suggestions the quick search asks for. Defaults to 10. */\n quickPageSize?: number;\n /** Milliseconds of quiet before a typed term is sent. Defaults to 250. */\n debounceMs?: number;\n}\n\nexport interface UseEntityLinkSourceOptions<TTarget extends Entity> {\n /**\n * The relation being edited. Its `linkSearchProperty` is what the quick search\n * filters on and its `linkLabelProperty` is what a target reads as.\n */\n descriptor: EntityFieldDescriptor;\n /** The foreign key the draft currently holds. */\n selectedId?: EntityId;\n /**\n * The instance behind {@link selectedId} when the caller already has it — the\n * one the user just picked, or the one a loaded record carried. Present, it\n * spares the label request entirely.\n */\n selectedEntity?: TTarget;\n}\n"],"names":[],"mappings":"AA+CA,WAcC"}
@@ -1,4 +1,4 @@
1
- import { EntifixError, type Entity, type EntityId } from '@entifix/core';
1
+ import { type Entity, type EntityId, type TransitionalFailure } from '@entifix/core';
2
2
  import type { UseEntityMutationOptions } from './use-entity-mutation.types.js';
3
3
  /**
4
4
  * Runs the write use-cases from a component — the mutating counterpart to
@@ -17,6 +17,6 @@ export declare function useEntityMutation<TEntity extends Entity, TContext>({ sa
17
17
  remove: (id: EntityId) => Promise<boolean>;
18
18
  isSaving: boolean;
19
19
  isDeleting: boolean;
20
- error: EntifixError | undefined;
20
+ error: TransitionalFailure | undefined;
21
21
  };
22
22
  //# sourceMappingURL=use-entity-mutation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-mutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIzE,OAAO,KAAK,EAEV,wBAAwB,EAEzB,MAAM,gCAAgC,CAAC;AAexC;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAClE,MAAM,EACN,QAAQ,EACR,GAAG,GACJ,EAAE,wBAAwB,CAAC,OAAO,EAAE,QAAQ,CAAC;mBAI3B,OAAO,KAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;iBAiB1C,QAAQ,KAAG,OAAO,CAAC,OAAO,CAAC;;;;EAiBzC"}
1
+ {"version":3,"file":"use-entity-mutation.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAEV,wBAAwB,EAEzB,MAAM,gCAAgC,CAAC;AAexC;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAClE,MAAM,EACN,QAAQ,EACR,GAAG,GACJ,EAAE,wBAAwB,CAAC,OAAO,EAAE,QAAQ,CAAC;mBAI3B,OAAO,KAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;iBAmB1C,QAAQ,KAAG,OAAO,CAAC,OAAO,CAAC;;;;EAmBzC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.ts"],"sourcesContent":["import { EntityIdTag, EntityTag } from '@entifix/business';\nimport { EntifixError, type Entity, type EntityId } from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useReducer } from 'react';\n\nimport type {\n UseEntityMutationAction,\n UseEntityMutationOptions,\n UseEntityMutationState,\n} from './use-entity-mutation.types.js';\n\nconst initialState: UseEntityMutationState = {\n isSaving: false,\n isDeleting: false,\n error: undefined,\n};\n\nfunction reducer(\n state: UseEntityMutationState,\n action: UseEntityMutationAction,\n): UseEntityMutationState {\n return { ...state, ...action.set };\n}\n\n/**\n * Runs the write use-cases from a component — the mutating counterpart to\n * {@link useDataLoading}.\n *\n * Inputs are provided as tags at call time ({@link EntityTag} for a save,\n * {@link EntityIdTag} for a delete), so the use-cases stay unaware of both React\n * and the transport behind `ctx`.\n *\n * `save` resolves to the entity the *repository* returned rather than the one\n * passed in. That distinction matters: a create round-trips through the store's\n * id generation, so only the returned entity is addressable.\n */\nexport function useEntityMutation<TEntity extends Entity, TContext>({\n saveUc,\n deleteUc,\n ctx,\n}: UseEntityMutationOptions<TEntity, TContext>) {\n const [state, dispatch] = useReducer(reducer, initialState);\n\n const save = useCallback(\n async (entity: TEntity): Promise<TEntity | undefined> => {\n dispatch({ set: { isSaving: true, error: undefined } });\n try {\n const saved = await Effect.runPromise(\n Effect.provide(saveUc, ctx.pipe(Context.add(EntityTag, entity))),\n );\n dispatch({ set: { isSaving: false } });\n return saved;\n } catch (error) {\n dispatch({ set: { isSaving: false, error: error as EntifixError } });\n return undefined;\n }\n },\n [saveUc, ctx],\n );\n\n const remove = useCallback(\n async (id: EntityId): Promise<boolean> => {\n dispatch({ set: { isDeleting: true, error: undefined } });\n try {\n await Effect.runPromise(\n Effect.provide(deleteUc, ctx.pipe(Context.add(EntityIdTag, id))),\n );\n dispatch({ set: { isDeleting: false } });\n return true;\n } catch (error) {\n dispatch({ set: { isDeleting: false, error: error as EntifixError } });\n return false;\n }\n },\n [deleteUc, ctx],\n );\n\n return { ...state, save, remove };\n}\n"],"names":["EntityIdTag","EntityTag","Context","Effect","useCallback","useReducer","initialState","isSaving","isDeleting","error","undefined","reducer","state","action","set","useEntityMutation","saveUc","deleteUc","ctx","dispatch","save","entity","saved","runPromise","provide","pipe","add","remove","id"],"mappings":";AAAA,SAASA,WAAW,EAAEC,SAAS,QAAQ,oBAAoB;AAE3D,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,UAAU,QAAQ,QAAQ;AAQhD,MAAMC,eAAuC;IAC3CC,UAAU;IACVC,YAAY;IACZC,OAAOC;AACT;AAEA,SAASC,QACPC,KAA6B,EAC7BC,MAA+B;IAE/B,OAAO,aAAKD,OAAUC,OAAOC,GAAG;AAClC;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAoD,EAClEC,MAAM,EACNC,QAAQ,EACRC,GAAG,EACyC;IAC5C,MAAM,CAACN,OAAOO,SAAS,GAAGd,WAAWM,SAASL;IAE9C,MAAMc,OAAOhB,YACX,OAAOiB;QACLF,SAAS;YAAEL,KAAK;gBAAEP,UAAU;gBAAME,OAAOC;YAAU;QAAE;QACrD,IAAI;YACF,MAAMY,QAAQ,MAAMnB,OAAOoB,UAAU,CACnCpB,OAAOqB,OAAO,CAACR,QAAQE,IAAIO,IAAI,CAACvB,QAAQwB,GAAG,CAACzB,WAAWoB;YAEzDF,SAAS;gBAAEL,KAAK;oBAAEP,UAAU;gBAAM;YAAE;YACpC,OAAOe;QACT,EAAE,OAAOb,OAAO;YACdU,SAAS;gBAAEL,KAAK;oBAAEP,UAAU;oBAAOE,OAAOA;gBAAsB;YAAE;YAClE,OAAOC;QACT;IACF,GACA;QAACM;QAAQE;KAAI;IAGf,MAAMS,SAASvB,YACb,OAAOwB;QACLT,SAAS;YAAEL,KAAK;gBAAEN,YAAY;gBAAMC,OAAOC;YAAU;QAAE;QACvD,IAAI;YACF,MAAMP,OAAOoB,UAAU,CACrBpB,OAAOqB,OAAO,CAACP,UAAUC,IAAIO,IAAI,CAACvB,QAAQwB,GAAG,CAAC1B,aAAa4B;YAE7DT,SAAS;gBAAEL,KAAK;oBAAEN,YAAY;gBAAM;YAAE;YACtC,OAAO;QACT,EAAE,OAAOC,OAAO;YACdU,SAAS;gBAAEL,KAAK;oBAAEN,YAAY;oBAAOC,OAAOA;gBAAsB;YAAE;YACpE,OAAO;QACT;IACF,GACA;QAACQ;QAAUC;KAAI;IAGjB,OAAO,aAAKN;QAAOQ;QAAMO;;AAC3B"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.ts"],"sourcesContent":["import { EntityIdTag, EntityTag } from '@entifix/business';\nimport {\n type Entity,\n type EntityId,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useReducer } from 'react';\n\nimport type {\n UseEntityMutationAction,\n UseEntityMutationOptions,\n UseEntityMutationState,\n} from './use-entity-mutation.types.js';\n\nconst initialState: UseEntityMutationState = {\n isSaving: false,\n isDeleting: false,\n error: undefined,\n};\n\nfunction reducer(\n state: UseEntityMutationState,\n action: UseEntityMutationAction,\n): UseEntityMutationState {\n return { ...state, ...action.set };\n}\n\n/**\n * Runs the write use-cases from a component — the mutating counterpart to\n * {@link useDataLoading}.\n *\n * Inputs are provided as tags at call time ({@link EntityTag} for a save,\n * {@link EntityIdTag} for a delete), so the use-cases stay unaware of both React\n * and the transport behind `ctx`.\n *\n * `save` resolves to the entity the *repository* returned rather than the one\n * passed in. That distinction matters: a create round-trips through the store's\n * id generation, so only the returned entity is addressable.\n */\nexport function useEntityMutation<TEntity extends Entity, TContext>({\n saveUc,\n deleteUc,\n ctx,\n}: UseEntityMutationOptions<TEntity, TContext>) {\n const [state, dispatch] = useReducer(reducer, initialState);\n\n const save = useCallback(\n async (entity: TEntity): Promise<TEntity | undefined> => {\n dispatch({ set: { isSaving: true, error: undefined } });\n try {\n const saved = await Effect.runPromise(\n Effect.provide(saveUc, ctx.pipe(Context.add(EntityTag, entity))),\n );\n dispatch({ set: { isSaving: false } });\n return saved;\n } catch (error) {\n dispatch({\n set: { isSaving: false, error: error as TransitionalFailure },\n });\n return undefined;\n }\n },\n [saveUc, ctx],\n );\n\n const remove = useCallback(\n async (id: EntityId): Promise<boolean> => {\n dispatch({ set: { isDeleting: true, error: undefined } });\n try {\n await Effect.runPromise(\n Effect.provide(deleteUc, ctx.pipe(Context.add(EntityIdTag, id))),\n );\n dispatch({ set: { isDeleting: false } });\n return true;\n } catch (error) {\n dispatch({\n set: { isDeleting: false, error: error as TransitionalFailure },\n });\n return false;\n }\n },\n [deleteUc, ctx],\n );\n\n return { ...state, save, remove };\n}\n"],"names":["EntityIdTag","EntityTag","Context","Effect","useCallback","useReducer","initialState","isSaving","isDeleting","error","undefined","reducer","state","action","set","useEntityMutation","saveUc","deleteUc","ctx","dispatch","save","entity","saved","runPromise","provide","pipe","add","remove","id"],"mappings":";AAAA,SAASA,WAAW,EAAEC,SAAS,QAAQ,oBAAoB;AAM3D,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,UAAU,QAAQ,QAAQ;AAQhD,MAAMC,eAAuC;IAC3CC,UAAU;IACVC,YAAY;IACZC,OAAOC;AACT;AAEA,SAASC,QACPC,KAA6B,EAC7BC,MAA+B;IAE/B,OAAO,aAAKD,OAAUC,OAAOC,GAAG;AAClC;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAoD,EAClEC,MAAM,EACNC,QAAQ,EACRC,GAAG,EACyC;IAC5C,MAAM,CAACN,OAAOO,SAAS,GAAGd,WAAWM,SAASL;IAE9C,MAAMc,OAAOhB,YACX,OAAOiB;QACLF,SAAS;YAAEL,KAAK;gBAAEP,UAAU;gBAAME,OAAOC;YAAU;QAAE;QACrD,IAAI;YACF,MAAMY,QAAQ,MAAMnB,OAAOoB,UAAU,CACnCpB,OAAOqB,OAAO,CAACR,QAAQE,IAAIO,IAAI,CAACvB,QAAQwB,GAAG,CAACzB,WAAWoB;YAEzDF,SAAS;gBAAEL,KAAK;oBAAEP,UAAU;gBAAM;YAAE;YACpC,OAAOe;QACT,EAAE,OAAOb,OAAO;YACdU,SAAS;gBACPL,KAAK;oBAAEP,UAAU;oBAAOE,OAAOA;gBAA6B;YAC9D;YACA,OAAOC;QACT;IACF,GACA;QAACM;QAAQE;KAAI;IAGf,MAAMS,SAASvB,YACb,OAAOwB;QACLT,SAAS;YAAEL,KAAK;gBAAEN,YAAY;gBAAMC,OAAOC;YAAU;QAAE;QACvD,IAAI;YACF,MAAMP,OAAOoB,UAAU,CACrBpB,OAAOqB,OAAO,CAACP,UAAUC,IAAIO,IAAI,CAACvB,QAAQwB,GAAG,CAAC1B,aAAa4B;YAE7DT,SAAS;gBAAEL,KAAK;oBAAEN,YAAY;gBAAM;YAAE;YACtC,OAAO;QACT,EAAE,OAAOC,OAAO;YACdU,SAAS;gBACPL,KAAK;oBAAEN,YAAY;oBAAOC,OAAOA;gBAA6B;YAChE;YACA,OAAO;QACT;IACF,GACA;QAACQ;QAAUC;KAAI;IAGjB,OAAO,aAAKN;QAAOQ;QAAMO;;AAC3B"}
@@ -1,19 +1,19 @@
1
1
  import { EntityIdTag, EntityTag } from '@entifix/business';
2
- import { EntifixError, Entity } from '@entifix/core';
2
+ import { Entity, type TransitionalFailure } from '@entifix/core';
3
3
  import { Context, Effect } from 'effect';
4
4
  export interface UseEntityMutationState {
5
5
  isSaving: boolean;
6
6
  isDeleting: boolean;
7
- error: EntifixError | undefined;
7
+ error: TransitionalFailure | undefined;
8
8
  }
9
9
  export type UseEntityMutationAction = {
10
10
  set: Partial<UseEntityMutationState>;
11
11
  };
12
12
  export interface UseEntityMutationOptions<TEntity extends Entity, TContext> {
13
13
  /** A save use-case; the hook supplies {@link EntityTag}. */
14
- saveUc: Effect.Effect<TEntity, EntifixError, EntityTag | TContext>;
14
+ saveUc: Effect.Effect<TEntity, TransitionalFailure, EntityTag | TContext>;
15
15
  /** A delete use-case; the hook supplies {@link EntityIdTag}. */
16
- deleteUc: Effect.Effect<void, EntifixError, EntityIdTag | TContext>;
16
+ deleteUc: Effect.Effect<void, TransitionalFailure, EntityIdTag | TContext>;
17
17
  ctx: Context.Context<TContext>;
18
18
  }
19
19
  //# sourceMappingURL=use-entity-mutation.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-mutation.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,YAAY,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACxE,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC;IACnE,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IACpE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChC"}
1
+ {"version":3,"file":"use-entity-mutation.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACxE,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC;IAC1E,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IAC3E,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.types.ts"],"sourcesContent":["import { EntityIdTag, EntityTag } from '@entifix/business';\nimport { EntifixError, Entity } from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseEntityMutationState {\n isSaving: boolean;\n isDeleting: boolean;\n error: EntifixError | undefined;\n}\n\nexport type UseEntityMutationAction = {\n set: Partial<UseEntityMutationState>;\n};\n\nexport interface UseEntityMutationOptions<TEntity extends Entity, TContext> {\n /** A save use-case; the hook supplies {@link EntityTag}. */\n saveUc: Effect.Effect<TEntity, EntifixError, EntityTag | TContext>;\n /** A delete use-case; the hook supplies {@link EntityIdTag}. */\n deleteUc: Effect.Effect<void, EntifixError, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n}\n"],"names":[],"mappings":"AAcA,WAMC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-mutation/use-entity-mutation.types.ts"],"sourcesContent":["import { EntityIdTag, EntityTag } from '@entifix/business';\nimport { Entity, type TransitionalFailure } from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseEntityMutationState {\n isSaving: boolean;\n isDeleting: boolean;\n error: TransitionalFailure | undefined;\n}\n\nexport type UseEntityMutationAction = {\n set: Partial<UseEntityMutationState>;\n};\n\nexport interface UseEntityMutationOptions<TEntity extends Entity, TContext> {\n /** A save use-case; the hook supplies {@link EntityTag}. */\n saveUc: Effect.Effect<TEntity, TransitionalFailure, EntityTag | TContext>;\n /** A delete use-case; the hook supplies {@link EntityIdTag}. */\n deleteUc: Effect.Effect<void, TransitionalFailure, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n}\n"],"names":[],"mappings":"AAcA,WAMC"}
@@ -1,4 +1,4 @@
1
- import { EntifixError, type Entity } from '@entifix/core';
1
+ import { type Entity, type TransitionalFailure } from '@entifix/core';
2
2
  import type { UseEntityRecordOptions } from './use-entity-record.types.js';
3
3
  /**
4
4
  * Loads a single entity by id — the read half of an entity form.
@@ -16,6 +16,6 @@ export declare function useEntityRecord<TEntity extends Entity, TContext>({ uc,
16
16
  setEntity: (entity: TEntity | undefined) => void;
17
17
  entity: TEntity | undefined;
18
18
  isLoading: boolean;
19
- error: EntifixError | undefined;
19
+ error: TransitionalFailure | undefined;
20
20
  };
21
21
  //# sourceMappingURL=use-entity-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-record.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-record/use-entity-record.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAI1D,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,8BAA8B,CAAC;AAmBtC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAChE,EAAE,EACF,GAAG,EACH,EAAE,GACH,EAAE,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC;;wBA8D/B,OAAO,GAAG,SAAS;;;;EAK/B"}
1
+ {"version":3,"file":"use-entity-record.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-record/use-entity-record.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAItE,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,8BAA8B,CAAC;AAmBtC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAChE,EAAE,EACF,GAAG,EACH,EAAE,GACH,EAAE,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC;;wBAgE/B,OAAO,GAAG,SAAS;;;;EAK/B"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-record/use-entity-record.ts"],"sourcesContent":["import { EntityIdTag } from '@entifix/business';\nimport { EntifixError, type Entity } from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useReducer, useRef, useState } from 'react';\n\nimport type {\n UseEntityRecordAction,\n UseEntityRecordOptions,\n UseEntityRecordState,\n} from './use-entity-record.types.js';\n\nfunction getInitialState<\n TEntity extends Entity,\n>(): UseEntityRecordState<TEntity> {\n return {\n entity: undefined,\n isLoading: false,\n error: undefined,\n };\n}\n\nfunction reducer<TEntity extends Entity>(\n state: UseEntityRecordState<TEntity>,\n action: UseEntityRecordAction<TEntity>,\n): UseEntityRecordState<TEntity> {\n return { ...state, ...action.set };\n}\n\n/**\n * Loads a single entity by id — the read half of an entity form.\n *\n * The id is provided through {@link EntityIdTag} at call time, mirroring how\n * {@link useDataLoading} provides the load request: the use-case stays\n * environment-agnostic and the hook is the only place that knows it is running\n * in React.\n *\n * When `id` is `undefined` the hook does not fetch. That is the create case —\n * there is no record yet — and it is a deliberate no-op rather than an error.\n */\nexport function useEntityRecord<TEntity extends Entity, TContext>({\n uc,\n ctx,\n id,\n}: UseEntityRecordOptions<TEntity, TContext>) {\n const [state, dispatch] = useReducer(\n reducer as (\n state: UseEntityRecordState<TEntity>,\n action: UseEntityRecordAction<TEntity>,\n ) => UseEntityRecordState<TEntity>,\n getInitialState<TEntity>(),\n );\n // Bumped to re-run the fetch on demand.\n const [reloadToken, setReloadToken] = useState(0);\n\n // Callers build `uc`/`ctx` inline, so they are fresh objects on every render.\n // Holding the latest in refs keeps them out of the fetch effect's\n // dependencies: keying the fetch on their identity would re-run it on every\n // render, and its dispatch would render again — an unbreakable loop. The\n // fetch is keyed on `id`/`reloadToken`, which is what actually selects a\n // record.\n const ucRef = useRef(uc);\n const ctxRef = useRef(ctx);\n\n // Refs may not be written during render; this runs before the fetch effect\n // below (effects fire in declaration order), so it always sees the latest.\n useEffect(() => {\n ucRef.current = uc;\n ctxRef.current = ctx;\n });\n\n useEffect(() => {\n if (id == null) {\n return;\n }\n\n // Guards against a slow response for a previous id landing after a newer one.\n let active = true;\n dispatch({ set: { isLoading: true, error: undefined } });\n\n Effect.runPromise(\n Effect.provide(\n ucRef.current,\n ctxRef.current.pipe(Context.add(EntityIdTag, id)),\n ),\n )\n .then(entity => {\n if (active) {\n dispatch({ set: { entity, isLoading: false } });\n }\n })\n .catch((error: unknown) => {\n if (active) {\n dispatch({ set: { error: error as EntifixError, isLoading: false } });\n }\n });\n\n return () => {\n active = false;\n };\n }, [id, reloadToken]);\n\n const reload = useCallback(() => setReloadToken(token => token + 1), []);\n\n /** Replaces the held record, e.g. with the entity a save returned. */\n const setEntity = useCallback(\n (entity: TEntity | undefined) => dispatch({ set: { entity } }),\n [],\n );\n\n return { ...state, reload, setEntity };\n}\n"],"names":["EntityIdTag","Context","Effect","useCallback","useEffect","useReducer","useRef","useState","getInitialState","entity","undefined","isLoading","error","reducer","state","action","set","useEntityRecord","uc","ctx","id","dispatch","reloadToken","setReloadToken","ucRef","ctxRef","current","active","runPromise","provide","pipe","add","then","catch","reload","token","setEntity"],"mappings":";AAAA,SAASA,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,SAAS,EAAEC,UAAU,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAQ7E,SAASC;IAGP,OAAO;QACLC,QAAQC;QACRC,WAAW;QACXC,OAAOF;IACT;AACF;AAEA,SAASG,QACPC,KAAoC,EACpCC,MAAsC;IAEtC,OAAO,aAAKD,OAAUC,OAAOC,GAAG;AAClC;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBAAkD,EAChEC,EAAE,EACFC,GAAG,EACHC,EAAE,EACwC;IAC1C,MAAM,CAACN,OAAOO,SAAS,GAAGhB,WACxBQ,SAIAL;IAEF,wCAAwC;IACxC,MAAM,CAACc,aAAaC,eAAe,GAAGhB,SAAS;IAE/C,8EAA8E;IAC9E,kEAAkE;IAClE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,UAAU;IACV,MAAMiB,QAAQlB,OAAOY;IACrB,MAAMO,SAASnB,OAAOa;IAEtB,2EAA2E;IAC3E,2EAA2E;IAC3Ef,UAAU;QACRoB,MAAME,OAAO,GAAGR;QAChBO,OAAOC,OAAO,GAAGP;IACnB;IAEAf,UAAU;QACR,IAAIgB,MAAM,MAAM;YACd;QACF;QAEA,8EAA8E;QAC9E,IAAIO,SAAS;QACbN,SAAS;YAAEL,KAAK;gBAAEL,WAAW;gBAAMC,OAAOF;YAAU;QAAE;QAEtDR,OAAO0B,UAAU,CACf1B,OAAO2B,OAAO,CACZL,MAAME,OAAO,EACbD,OAAOC,OAAO,CAACI,IAAI,CAAC7B,QAAQ8B,GAAG,CAAC/B,aAAaoB,OAG9CY,IAAI,CAACvB,CAAAA;YACJ,IAAIkB,QAAQ;gBACVN,SAAS;oBAAEL,KAAK;wBAAEP;wBAAQE,WAAW;oBAAM;gBAAE;YAC/C;QACF,GACCsB,KAAK,CAAC,CAACrB;YACN,IAAIe,QAAQ;gBACVN,SAAS;oBAAEL,KAAK;wBAAEJ,OAAOA;wBAAuBD,WAAW;oBAAM;gBAAE;YACrE;QACF;QAEF,OAAO;YACLgB,SAAS;QACX;IACF,GAAG;QAACP;QAAIE;KAAY;IAEpB,MAAMY,SAAS/B,YAAY,IAAMoB,eAAeY,CAAAA,QAASA,QAAQ,IAAI,EAAE;IAEvE,oEAAoE,GACpE,MAAMC,YAAYjC,YAChB,CAACM,SAAgCY,SAAS;YAAEL,KAAK;gBAAEP;YAAO;QAAE,IAC5D,EAAE;IAGJ,OAAO,aAAKK;QAAOoB;QAAQE;;AAC7B"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-record/use-entity-record.ts"],"sourcesContent":["import { EntityIdTag } from '@entifix/business';\nimport { type Entity, type TransitionalFailure } from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useReducer, useRef, useState } from 'react';\n\nimport type {\n UseEntityRecordAction,\n UseEntityRecordOptions,\n UseEntityRecordState,\n} from './use-entity-record.types.js';\n\nfunction getInitialState<\n TEntity extends Entity,\n>(): UseEntityRecordState<TEntity> {\n return {\n entity: undefined,\n isLoading: false,\n error: undefined,\n };\n}\n\nfunction reducer<TEntity extends Entity>(\n state: UseEntityRecordState<TEntity>,\n action: UseEntityRecordAction<TEntity>,\n): UseEntityRecordState<TEntity> {\n return { ...state, ...action.set };\n}\n\n/**\n * Loads a single entity by id — the read half of an entity form.\n *\n * The id is provided through {@link EntityIdTag} at call time, mirroring how\n * {@link useDataLoading} provides the load request: the use-case stays\n * environment-agnostic and the hook is the only place that knows it is running\n * in React.\n *\n * When `id` is `undefined` the hook does not fetch. That is the create case —\n * there is no record yet — and it is a deliberate no-op rather than an error.\n */\nexport function useEntityRecord<TEntity extends Entity, TContext>({\n uc,\n ctx,\n id,\n}: UseEntityRecordOptions<TEntity, TContext>) {\n const [state, dispatch] = useReducer(\n reducer as (\n state: UseEntityRecordState<TEntity>,\n action: UseEntityRecordAction<TEntity>,\n ) => UseEntityRecordState<TEntity>,\n getInitialState<TEntity>(),\n );\n // Bumped to re-run the fetch on demand.\n const [reloadToken, setReloadToken] = useState(0);\n\n // Callers build `uc`/`ctx` inline, so they are fresh objects on every render.\n // Holding the latest in refs keeps them out of the fetch effect's\n // dependencies: keying the fetch on their identity would re-run it on every\n // render, and its dispatch would render again — an unbreakable loop. The\n // fetch is keyed on `id`/`reloadToken`, which is what actually selects a\n // record.\n const ucRef = useRef(uc);\n const ctxRef = useRef(ctx);\n\n // Refs may not be written during render; this runs before the fetch effect\n // below (effects fire in declaration order), so it always sees the latest.\n useEffect(() => {\n ucRef.current = uc;\n ctxRef.current = ctx;\n });\n\n useEffect(() => {\n if (id == null) {\n return;\n }\n\n // Guards against a slow response for a previous id landing after a newer one.\n let active = true;\n dispatch({ set: { isLoading: true, error: undefined } });\n\n Effect.runPromise(\n Effect.provide(\n ucRef.current,\n ctxRef.current.pipe(Context.add(EntityIdTag, id)),\n ),\n )\n .then(entity => {\n if (active) {\n dispatch({ set: { entity, isLoading: false } });\n }\n })\n .catch((error: unknown) => {\n if (active) {\n dispatch({\n set: { error: error as TransitionalFailure, isLoading: false },\n });\n }\n });\n\n return () => {\n active = false;\n };\n }, [id, reloadToken]);\n\n const reload = useCallback(() => setReloadToken(token => token + 1), []);\n\n /** Replaces the held record, e.g. with the entity a save returned. */\n const setEntity = useCallback(\n (entity: TEntity | undefined) => dispatch({ set: { entity } }),\n [],\n );\n\n return { ...state, reload, setEntity };\n}\n"],"names":["EntityIdTag","Context","Effect","useCallback","useEffect","useReducer","useRef","useState","getInitialState","entity","undefined","isLoading","error","reducer","state","action","set","useEntityRecord","uc","ctx","id","dispatch","reloadToken","setReloadToken","ucRef","ctxRef","current","active","runPromise","provide","pipe","add","then","catch","reload","token","setEntity"],"mappings":";AAAA,SAASA,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,SAAS,EAAEC,UAAU,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAQ7E,SAASC;IAGP,OAAO;QACLC,QAAQC;QACRC,WAAW;QACXC,OAAOF;IACT;AACF;AAEA,SAASG,QACPC,KAAoC,EACpCC,MAAsC;IAEtC,OAAO,aAAKD,OAAUC,OAAOC,GAAG;AAClC;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBAAkD,EAChEC,EAAE,EACFC,GAAG,EACHC,EAAE,EACwC;IAC1C,MAAM,CAACN,OAAOO,SAAS,GAAGhB,WACxBQ,SAIAL;IAEF,wCAAwC;IACxC,MAAM,CAACc,aAAaC,eAAe,GAAGhB,SAAS;IAE/C,8EAA8E;IAC9E,kEAAkE;IAClE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,UAAU;IACV,MAAMiB,QAAQlB,OAAOY;IACrB,MAAMO,SAASnB,OAAOa;IAEtB,2EAA2E;IAC3E,2EAA2E;IAC3Ef,UAAU;QACRoB,MAAME,OAAO,GAAGR;QAChBO,OAAOC,OAAO,GAAGP;IACnB;IAEAf,UAAU;QACR,IAAIgB,MAAM,MAAM;YACd;QACF;QAEA,8EAA8E;QAC9E,IAAIO,SAAS;QACbN,SAAS;YAAEL,KAAK;gBAAEL,WAAW;gBAAMC,OAAOF;YAAU;QAAE;QAEtDR,OAAO0B,UAAU,CACf1B,OAAO2B,OAAO,CACZL,MAAME,OAAO,EACbD,OAAOC,OAAO,CAACI,IAAI,CAAC7B,QAAQ8B,GAAG,CAAC/B,aAAaoB,OAG9CY,IAAI,CAACvB,CAAAA;YACJ,IAAIkB,QAAQ;gBACVN,SAAS;oBAAEL,KAAK;wBAAEP;wBAAQE,WAAW;oBAAM;gBAAE;YAC/C;QACF,GACCsB,KAAK,CAAC,CAACrB;YACN,IAAIe,QAAQ;gBACVN,SAAS;oBACPL,KAAK;wBAAEJ,OAAOA;wBAA8BD,WAAW;oBAAM;gBAC/D;YACF;QACF;QAEF,OAAO;YACLgB,SAAS;QACX;IACF,GAAG;QAACP;QAAIE;KAAY;IAEpB,MAAMY,SAAS/B,YAAY,IAAMoB,eAAeY,CAAAA,QAASA,QAAQ,IAAI,EAAE;IAEvE,oEAAoE,GACpE,MAAMC,YAAYjC,YAChB,CAACM,SAAgCY,SAAS;YAAEL,KAAK;gBAAEP;YAAO;QAAE,IAC5D,EAAE;IAGJ,OAAO,aAAKK;QAAOoB;QAAQE;;AAC7B"}
@@ -1,17 +1,17 @@
1
1
  import { EntityIdTag } from '@entifix/business';
2
- import { EntifixError, Entity, EntityId } from '@entifix/core';
2
+ import { Entity, EntityId, type TransitionalFailure } from '@entifix/core';
3
3
  import { Context, Effect } from 'effect';
4
4
  export interface UseEntityRecordState<TEntity extends Entity> {
5
5
  entity: TEntity | undefined;
6
6
  isLoading: boolean;
7
- error: EntifixError | undefined;
7
+ error: TransitionalFailure | undefined;
8
8
  }
9
9
  export type UseEntityRecordAction<TEntity extends Entity> = {
10
10
  set: Partial<UseEntityRecordState<TEntity>>;
11
11
  };
12
12
  export interface UseEntityRecordOptions<TEntity extends Entity, TContext> {
13
13
  /** A get use-case; the hook supplies {@link EntityIdTag}. */
14
- uc: Effect.Effect<TEntity, EntifixError, EntityIdTag | TContext>;
14
+ uc: Effect.Effect<TEntity, TransitionalFailure, EntityIdTag | TContext>;
15
15
  ctx: Context.Context<TContext>;
16
16
  /**
17
17
  * The record to load. `undefined` means "nothing to load yet" — the create
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-record.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-record/use-entity-record.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,MAAM;IAC1D,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,YAAY,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,IAAI;IAC1D,GAAG,EAAE,OAAO,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;CAC7C,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACtE,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IACjE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,EAAE,EAAE,QAAQ,CAAC;CACd"}
1
+ {"version":3,"file":"use-entity-record.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-record/use-entity-record.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,MAAM;IAC1D,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,IAAI;IAC1D,GAAG,EAAE,OAAO,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;CAC7C,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACtE,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;IACxE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,EAAE,EAAE,QAAQ,CAAC;CACd"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-record/use-entity-record.types.ts"],"sourcesContent":["import { EntityIdTag } from '@entifix/business';\nimport { EntifixError, Entity, EntityId } from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseEntityRecordState<TEntity extends Entity> {\n entity: TEntity | undefined;\n isLoading: boolean;\n error: EntifixError | undefined;\n}\n\nexport type UseEntityRecordAction<TEntity extends Entity> = {\n set: Partial<UseEntityRecordState<TEntity>>;\n};\n\nexport interface UseEntityRecordOptions<TEntity extends Entity, TContext> {\n /** A get use-case; the hook supplies {@link EntityIdTag}. */\n uc: Effect.Effect<TEntity, EntifixError, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n /**\n * The record to load. `undefined` means \"nothing to load yet\" — the create\n * case — and the hook stays idle rather than fetching.\n */\n id: EntityId;\n}\n"],"names":[],"mappings":"AAcA,WASC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-record/use-entity-record.types.ts"],"sourcesContent":["import { EntityIdTag } from '@entifix/business';\nimport { Entity, EntityId, type TransitionalFailure } from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseEntityRecordState<TEntity extends Entity> {\n entity: TEntity | undefined;\n isLoading: boolean;\n error: TransitionalFailure | undefined;\n}\n\nexport type UseEntityRecordAction<TEntity extends Entity> = {\n set: Partial<UseEntityRecordState<TEntity>>;\n};\n\nexport interface UseEntityRecordOptions<TEntity extends Entity, TContext> {\n /** A get use-case; the hook supplies {@link EntityIdTag}. */\n uc: Effect.Effect<TEntity, TransitionalFailure, EntityIdTag | TContext>;\n ctx: Context.Context<TContext>;\n /**\n * The record to load. `undefined` means \"nothing to load yet\" — the create\n * case — and the hook stays idle rather than fetching.\n */\n id: EntityId;\n}\n"],"names":[],"mappings":"AAcA,WASC"}
@@ -3,7 +3,7 @@ import { type ReactNode } from 'react';
3
3
  /**
4
4
  * A `QueryClient` tuned for the entity data layer: server data is cached and
5
5
  * served stale-while-revalidate (so a revisited list paints from cache with no
6
- * spinner), and failed queries surface their {@link EntifixError} immediately
6
+ * spinner), and failed queries surface their failure immediately
7
7
  * rather than being retried behind a spinner.
8
8
  */
9
9
  export declare function makeQueryClient(config?: QueryClientConfig): QueryClient;
@@ -6,7 +6,7 @@ import { useState } from 'react';
6
6
  /**
7
7
  * A `QueryClient` tuned for the entity data layer: server data is cached and
8
8
  * served stale-while-revalidate (so a revisited list paints from cache with no
9
- * spinner), and failed queries surface their {@link EntifixError} immediately
9
+ * spinner), and failed queries surface their failure immediately
10
10
  * rather than being retried behind a spinner.
11
11
  */ export function makeQueryClient(config) {
12
12
  var _config_defaultOptions;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/query/query-provider.tsx"],"sourcesContent":["'use client';\n\nimport {\n QueryClient,\n type QueryClientConfig,\n QueryClientProvider,\n} from '@tanstack/react-query';\nimport { type ReactNode, useState } from 'react';\n\n/**\n * A `QueryClient` tuned for the entity data layer: server data is cached and\n * served stale-while-revalidate (so a revisited list paints from cache with no\n * spinner), and failed queries surface their {@link EntifixError} immediately\n * rather than being retried behind a spinner.\n */\nexport function makeQueryClient(config?: QueryClientConfig): QueryClient {\n return new QueryClient({\n ...config,\n defaultOptions: {\n ...config?.defaultOptions,\n queries: {\n staleTime: 30_000,\n retry: false,\n refetchOnWindowFocus: false,\n ...config?.defaultOptions?.queries,\n },\n },\n });\n}\n\nexport interface EntifixQueryProviderProps {\n children: ReactNode;\n /** Inject a client (tests, SSR); otherwise one stable client is created. */\n client?: QueryClient;\n}\n\n/**\n * Mounts the entity data layer's `QueryClient`. Mount once at the app root,\n * above the adapters provider. Holding the client in `useState` keeps a single\n * instance across renders without recreating it (which would drop the cache).\n */\nexport function EntifixQueryProvider({\n children,\n client,\n}: EntifixQueryProviderProps) {\n const [queryClient] = useState(() => client ?? makeQueryClient());\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n );\n}\n"],"names":["QueryClient","QueryClientProvider","useState","makeQueryClient","config","defaultOptions","queries","staleTime","retry","refetchOnWindowFocus","EntifixQueryProvider","children","client","queryClient"],"mappings":"AAAA;;;AAEA,SACEA,WAAW,EAEXC,mBAAmB,QACd,wBAAwB;AAC/B,SAAyBC,QAAQ,QAAQ,QAAQ;AAEjD;;;;;CAKC,GACD,OAAO,SAASC,gBAAgBC,MAA0B;QAS/CA;IART,OAAO,IAAIJ,YAAY,aAClBI;QACHC,gBAAgB,aACXD,0BAAAA,OAAQC,cAAc;YACzBC,SAAS;gBACPC,WAAW;gBACXC,OAAO;gBACPC,sBAAsB;eACnBL,2BAAAA,yBAAAA,OAAQC,cAAc,qBAAtBD,uBAAwBE,OAAO;;;AAI1C;AAQA;;;;CAIC,GACD,OAAO,SAASI,qBAAqB,EACnCC,QAAQ,EACRC,MAAM,EACoB;IAC1B,MAAM,CAACC,YAAY,GAAGX,SAAS,IAAMU,iBAAAA,SAAUT;IAC/C,qBACE,KAACF;QAAoBW,QAAQC;kBAAcF;;AAE/C"}
1
+ {"version":3,"sources":["../../src/query/query-provider.tsx"],"sourcesContent":["'use client';\n\nimport {\n QueryClient,\n type QueryClientConfig,\n QueryClientProvider,\n} from '@tanstack/react-query';\nimport { type ReactNode, useState } from 'react';\n\n/**\n * A `QueryClient` tuned for the entity data layer: server data is cached and\n * served stale-while-revalidate (so a revisited list paints from cache with no\n * spinner), and failed queries surface their failure immediately\n * rather than being retried behind a spinner.\n */\nexport function makeQueryClient(config?: QueryClientConfig): QueryClient {\n return new QueryClient({\n ...config,\n defaultOptions: {\n ...config?.defaultOptions,\n queries: {\n staleTime: 30_000,\n retry: false,\n refetchOnWindowFocus: false,\n ...config?.defaultOptions?.queries,\n },\n },\n });\n}\n\nexport interface EntifixQueryProviderProps {\n children: ReactNode;\n /** Inject a client (tests, SSR); otherwise one stable client is created. */\n client?: QueryClient;\n}\n\n/**\n * Mounts the entity data layer's `QueryClient`. Mount once at the app root,\n * above the adapters provider. Holding the client in `useState` keeps a single\n * instance across renders without recreating it (which would drop the cache).\n */\nexport function EntifixQueryProvider({\n children,\n client,\n}: EntifixQueryProviderProps) {\n const [queryClient] = useState(() => client ?? makeQueryClient());\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n );\n}\n"],"names":["QueryClient","QueryClientProvider","useState","makeQueryClient","config","defaultOptions","queries","staleTime","retry","refetchOnWindowFocus","EntifixQueryProvider","children","client","queryClient"],"mappings":"AAAA;;;AAEA,SACEA,WAAW,EAEXC,mBAAmB,QACd,wBAAwB;AAC/B,SAAyBC,QAAQ,QAAQ,QAAQ;AAEjD;;;;;CAKC,GACD,OAAO,SAASC,gBAAgBC,MAA0B;QAS/CA;IART,OAAO,IAAIJ,YAAY,aAClBI;QACHC,gBAAgB,aACXD,0BAAAA,OAAQC,cAAc;YACzBC,SAAS;gBACPC,WAAW;gBACXC,OAAO;gBACPC,sBAAsB;eACnBL,2BAAAA,yBAAAA,OAAQC,cAAc,qBAAtBD,uBAAwBE,OAAO;;;AAI1C;AAQA;;;;CAIC,GACD,OAAO,SAASI,qBAAqB,EACnCC,QAAQ,EACRC,MAAM,EACoB;IAC1B,MAAM,CAACC,YAAY,GAAGX,SAAS,IAAMU,iBAAAA,SAAUT;IAC/C,qBACE,KAACF;QAAoBW,QAAQC;kBAAcF;;AAE/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"url-query.d.ts","sourceRoot":"","sources":["../../src/url/url-query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"url-query.d.ts","sourceRoot":"","sources":["../../src/url/url-query.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { EntifixLogicError } from '@entifix/core';
3
2
  /** Whether a parameter orders the answer rather than filters it. */ function isSortParam(param) {
4
3
  return 'sorting' in param;
5
4
  }
@@ -24,17 +23,12 @@ import { EntifixLogicError } from '@entifix/core';
24
23
  const pageKeys = keys.filter((key)=>isPageParam(params[key]));
25
24
  // Two would be two answers to "which page", and the request has one.
26
25
  if (pageKeys.length > 1) {
27
- throw new EntifixLogicError(`A URL query declares one page parameter at most; found ${pageKeys.join(', ')}.`, undefined, {
28
- pageKeys
29
- });
26
+ throw new Error(`A URL query declares one page parameter at most; found ${pageKeys.join(', ')}.`);
30
27
  }
31
28
  const [pageKey] = pageKeys;
32
29
  const pageSize = pageKey === undefined ? undefined : params[pageKey].pageSize;
33
30
  if (pageSize !== undefined && !(Number.isInteger(pageSize) && pageSize > 0)) {
34
- throw new EntifixLogicError(`The page parameter "${pageKey}" needs a positive integer pageSize; got ${pageSize}.`, undefined, {
35
- pageKey,
36
- pageSize
37
- });
31
+ throw new Error(`The page parameter "${pageKey}" needs a positive integer pageSize; got ${pageSize}.`);
38
32
  }
39
33
  const stateOf = (values)=>Object.fromEntries(keys.map((key)=>[
40
34
  key,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/url/url-query.ts"],"sourcesContent":["import {\n EntifixLogicError,\n type Entity,\n type EntityFilter,\n type EntityFiltering,\n type EntityLoadRequest,\n type EntitySorting,\n type FilterGroup,\n} from '@entifix/core';\n\n/**\n * Any entity, as far as a parameter can tell: it names its property by text,\n * since a dotted path (`title.en`) is no key of the entity's type.\n */\ntype AnyEntity = Entity & Record<string, unknown>;\n\n/** What one parameter of the query string turns into. */\nexport type Condition = EntityFilter<AnyEntity> | FilterGroup<AnyEntity>;\n\n/**\n * One parameter a query string may carry: the values it accepts and the\n * condition they become. `allowed` drops anything else, so a hand-edited or\n * stale link filters on what still exists; `single` keeps the first value\n * only, as a search box has one.\n */\nexport interface FilterParam<TContext> {\n readonly allowed?: readonly string[];\n readonly single?: boolean;\n readonly condition: (\n values: readonly string[],\n context: TContext,\n ) => Condition;\n}\n\n/**\n * A parameter that orders rather than filters: each value it accepts names a\n * sorting (`?sort=date-desc`). It holds one value, and any other is dropped.\n */\nexport interface SortParam {\n readonly sorting: Readonly<Record<string, EntitySorting<AnyEntity>[]>>;\n}\n\n/**\n * A parameter that picks one record to show rather than filters or orders\n * the answer: `?adr=0016`. It holds one value, `allowed` when given, and never\n * reaches the request.\n */\nexport interface SelectParam {\n readonly select: true;\n readonly allowed?: readonly string[];\n}\n\n/**\n * A parameter that picks the page of the answer: `?page=3`, `pageSize` records\n * at a time. It holds one positive integer, and page 1 is written as no value\n * at all, so `?page=1` and the bare URL are one address rather than two\n * cached copies of the same answer.\n *\n * A page belongs to one answer. Changing a filter or the sort changes the\n * answer, so {@link UrlQuery.withParam} and {@link UrlQuery.withoutFilters}\n * drop it: page 4 of a narrower result is usually past its end.\n */\nexport interface PageParam {\n readonly pageSize: number;\n}\n\nexport type UrlParam<TContext> =\n FilterParam<TContext> | SortParam | SelectParam | PageParam;\n\n/** Whether a parameter orders the answer rather than filters it. */\nfunction isSortParam<TContext>(param: UrlParam<TContext>): param is SortParam {\n return 'sorting' in param;\n}\n\n/** Whether a parameter picks the page of the answer. */\nfunction isPageParam<TContext>(param: UrlParam<TContext>): param is PageParam {\n return 'pageSize' in param;\n}\n\n/** A page number as a URL writes it: a positive integer, without a sign or zeros. */\nconst PAGE_NUMBER = /^[1-9]\\d*$/;\n\n/** Whether a parameter picks a record rather than asks the load anything. */\nfunction isSelectParam<TContext>(\n param: UrlParam<TContext>,\n): param is SelectParam {\n return 'select' in param;\n}\n\n/** The values of every parameter, in the order the URL gave them. */\nexport type UrlState<TKey extends string> = Readonly<\n Record<TKey, readonly string[]>\n>;\n\nexport interface UrlQuery<TKey extends string, TContext> {\n /** Every parameter with no value: no filter at all. */\n readonly empty: UrlState<TKey>;\n parse(search: string): UrlState<TKey>;\n /** `''` for an empty state, else `?` and the parameters in declared order. */\n serialize(state: UrlState<TKey>): string;\n isEmpty(state: UrlState<TKey>): boolean;\n /** Whether any parameter that filters has a value; a sort alone does not. */\n isFiltering(state: UrlState<TKey>): boolean;\n /**\n * Whether the state asks the load anything: a filter, a sort or a page. A\n * selection alone does not.\n */\n isAsking(state: UrlState<TKey>): boolean;\n /**\n * The state with every filter emptied, its sort and selection kept, and its\n * page dropped.\n */\n withoutFilters(state: UrlState<TKey>): UrlState<TKey>;\n /**\n * The state with one parameter set to `values`, checked the way `parse`\n * checks them. Setting a filter or the sort also drops the page, because\n * it changes the answer the page was a page of.\n */\n withParam(\n state: UrlState<TKey>,\n key: TKey,\n values: readonly string[],\n ): UrlState<TKey>;\n /**\n * The load request the state stands for: one condition per parameter set,\n * the sort, and — when a page parameter is declared — `page` and `pageSize`.\n */\n request<TEntity extends Entity>(\n state: UrlState<TKey>,\n context: TContext,\n ): EntityLoadRequest<TEntity>;\n}\n\nexport interface UrlQueryOptions<TKey extends string, TContext> {\n readonly params: Readonly<Record<TKey, UrlParam<TContext>>>;\n /** Applied to every request, filtered or not, unless a sort is chosen. */\n readonly sorting?: EntitySorting<AnyEntity>[];\n}\n\n/**\n * A filter kept in the query string, read as an entifix `EntityLoadRequest`.\n *\n * entifix's own `parseLoadRequestParams` writes RSQL and accepts only members\n * marked `filterable`, which no collection or localized member may be. This\n * declares its parameters instead: readable URLs (`?tag=a&tag=b&q=text`) whose\n * every value is checked, each becoming one condition of the request.\n */\nexport function defineUrlQuery<TKey extends string, TContext = undefined>({\n params,\n sorting: defaultSorting,\n}: UrlQueryOptions<TKey, TContext>): UrlQuery<TKey, TContext> {\n const keys = Object.keys(params) as TKey[];\n const filterKeys = keys.filter(\n key =>\n !isSortParam(params[key]) &&\n !isSelectParam(params[key]) &&\n !isPageParam(params[key]),\n );\n const askingKeys = keys.filter(key => !isSelectParam(params[key]));\n const pageKeys = keys.filter(key => isPageParam(params[key]));\n // Two would be two answers to \"which page\", and the request has one.\n if (pageKeys.length > 1) {\n throw new EntifixLogicError(\n `A URL query declares one page parameter at most; found ${pageKeys.join(', ')}.`,\n undefined,\n { pageKeys },\n );\n }\n const [pageKey] = pageKeys;\n const pageSize =\n pageKey === undefined ? undefined : (params[pageKey] as PageParam).pageSize;\n if (pageSize !== undefined && !(Number.isInteger(pageSize) && pageSize > 0)) {\n throw new EntifixLogicError(\n `The page parameter \"${pageKey}\" needs a positive integer pageSize; got ${pageSize}.`,\n undefined,\n { pageKey, pageSize },\n );\n }\n const stateOf = (values: (key: TKey) => readonly string[]) =>\n Object.fromEntries(\n keys.map(key => [key, values(key)]),\n ) as unknown as UrlState<TKey>;\n const empty = stateOf(() => []);\n\n const accepted = (key: TKey, values: readonly string[]) => {\n const param = params[key];\n if (isPageParam(param)) {\n // Page 1 is the bare URL, so it is dropped rather than kept as a second\n // spelling of the same address.\n const [first] = values.filter(value => PAGE_NUMBER.test(value));\n return first === undefined || first === '1' ? [] : [first];\n }\n const { allowed, single } = isSortParam(param)\n ? { allowed: Object.keys(param.sorting), single: true }\n : isSelectParam(param)\n ? { allowed: param.allowed, single: true }\n : param;\n // Kept as written: a search box being typed into holds its spaces.\n const kept = values\n .filter(value => value.trim() !== '')\n .filter(value => allowed === undefined || allowed.includes(value));\n const unique = [...new Set(kept)];\n return single ? unique.slice(0, 1) : unique;\n };\n\n return {\n empty,\n parse(search) {\n const query = new URLSearchParams(search);\n return stateOf(key => accepted(key, query.getAll(key)));\n },\n serialize(state) {\n const query = new URLSearchParams();\n for (const key of keys) {\n for (const value of state[key]) query.append(key, value);\n }\n const text = query.toString();\n return text === '' ? '' : `?${text}`;\n },\n isEmpty(state) {\n return keys.every(key => state[key].length === 0);\n },\n isFiltering(state) {\n return filterKeys.some(key => state[key].length > 0);\n },\n isAsking(state) {\n return askingKeys.some(key => state[key].length > 0);\n },\n withoutFilters(state) {\n return stateOf(key =>\n filterKeys.includes(key) || key === pageKey ? [] : state[key],\n );\n },\n withParam(state, key, values) {\n const resetsPage = key !== pageKey && !isSelectParam(params[key]);\n return stateOf(other =>\n other === key\n ? accepted(key, values)\n : other === pageKey && resetsPage\n ? []\n : state[other],\n );\n },\n request<TEntity extends Entity>(state: UrlState<TKey>, context: TContext) {\n const filtering = filterKeys.flatMap(key => {\n const param = params[key] as FilterParam<TContext>;\n return state[key].length === 0\n ? []\n : [param.condition(state[key], context)];\n });\n const chosen = keys.flatMap(key => {\n const param = params[key];\n const [value] = state[key];\n return isSortParam(param) && value !== undefined\n ? [param.sorting[value] as EntitySorting<AnyEntity>[]]\n : [];\n });\n const sorting = chosen[0] ?? defaultSorting;\n const [page] = pageKey === undefined ? [] : state[pageKey];\n return {\n ...(pageSize !== undefined && {\n page: page === undefined ? 1 : Number(page),\n pageSize,\n }),\n ...(filtering.length > 0 && {\n filtering: filtering as unknown as EntityFiltering<TEntity>[],\n }),\n ...(sorting !== undefined && {\n sorting: sorting as unknown as EntitySorting<TEntity>[],\n }),\n };\n },\n };\n}\n\n/** Any of the values: `in`, which matches any element of a collection too. */\nexport function anyOf(property: string) {\n return (values: readonly string[]): Condition => ({\n property,\n operator: 'in',\n values: [...values],\n });\n}\n\n/**\n * The text as a substring of a member, as the repository's `like` matches\n * it. `property` names the member for a context, as a locale picks the\n * member holding its text.\n */\nexport function containing<TContext>(\n property: (context: TContext) => string,\n): FilterParam<TContext>['condition'] {\n return ([value], context) => ({\n property: property(context),\n operator: 'like',\n value: (value as string).trim(),\n });\n}\n\n/** A date inside any of the given years (UTC), as `between` per year. */\nexport function inAnyYear(property: string) {\n return (values: readonly string[]): Condition => ({\n operator: 'or',\n values: values.map(year => ({\n property,\n operator: 'between' as const,\n start: new Date(`${year}-01-01T00:00:00.000Z`),\n end: new Date(`${year}-12-31T23:59:59.999Z`),\n })),\n });\n}\n"],"names":["EntifixLogicError","isSortParam","param","isPageParam","PAGE_NUMBER","isSelectParam","defineUrlQuery","params","sorting","defaultSorting","keys","Object","filterKeys","filter","key","askingKeys","pageKeys","length","join","undefined","pageKey","pageSize","Number","isInteger","stateOf","values","fromEntries","map","empty","accepted","first","value","test","allowed","single","kept","trim","includes","unique","Set","slice","parse","search","query","URLSearchParams","getAll","serialize","state","append","text","toString","isEmpty","every","isFiltering","some","isAsking","withoutFilters","withParam","resetsPage","other","request","context","chosen","filtering","flatMap","condition","page","anyOf","property","operator","containing","inAnyYear","year","start","Date","end"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/url/url-query.ts"],"sourcesContent":["import {\n type Entity,\n type EntityFilter,\n type EntityFiltering,\n type EntityLoadRequest,\n type EntitySorting,\n type FilterGroup,\n} from '@entifix/core';\n\n/**\n * Any entity, as far as a parameter can tell: it names its property by text,\n * since a dotted path (`title.en`) is no key of the entity's type.\n */\ntype AnyEntity = Entity & Record<string, unknown>;\n\n/** What one parameter of the query string turns into. */\nexport type Condition = EntityFilter<AnyEntity> | FilterGroup<AnyEntity>;\n\n/**\n * One parameter a query string may carry: the values it accepts and the\n * condition they become. `allowed` drops anything else, so a hand-edited or\n * stale link filters on what still exists; `single` keeps the first value\n * only, as a search box has one.\n */\nexport interface FilterParam<TContext> {\n readonly allowed?: readonly string[];\n readonly single?: boolean;\n readonly condition: (\n values: readonly string[],\n context: TContext,\n ) => Condition;\n}\n\n/**\n * A parameter that orders rather than filters: each value it accepts names a\n * sorting (`?sort=date-desc`). It holds one value, and any other is dropped.\n */\nexport interface SortParam {\n readonly sorting: Readonly<Record<string, EntitySorting<AnyEntity>[]>>;\n}\n\n/**\n * A parameter that picks one record to show rather than filters or orders\n * the answer: `?adr=0016`. It holds one value, `allowed` when given, and never\n * reaches the request.\n */\nexport interface SelectParam {\n readonly select: true;\n readonly allowed?: readonly string[];\n}\n\n/**\n * A parameter that picks the page of the answer: `?page=3`, `pageSize` records\n * at a time. It holds one positive integer, and page 1 is written as no value\n * at all, so `?page=1` and the bare URL are one address rather than two\n * cached copies of the same answer.\n *\n * A page belongs to one answer. Changing a filter or the sort changes the\n * answer, so {@link UrlQuery.withParam} and {@link UrlQuery.withoutFilters}\n * drop it: page 4 of a narrower result is usually past its end.\n */\nexport interface PageParam {\n readonly pageSize: number;\n}\n\nexport type UrlParam<TContext> =\n FilterParam<TContext> | SortParam | SelectParam | PageParam;\n\n/** Whether a parameter orders the answer rather than filters it. */\nfunction isSortParam<TContext>(param: UrlParam<TContext>): param is SortParam {\n return 'sorting' in param;\n}\n\n/** Whether a parameter picks the page of the answer. */\nfunction isPageParam<TContext>(param: UrlParam<TContext>): param is PageParam {\n return 'pageSize' in param;\n}\n\n/** A page number as a URL writes it: a positive integer, without a sign or zeros. */\nconst PAGE_NUMBER = /^[1-9]\\d*$/;\n\n/** Whether a parameter picks a record rather than asks the load anything. */\nfunction isSelectParam<TContext>(\n param: UrlParam<TContext>,\n): param is SelectParam {\n return 'select' in param;\n}\n\n/** The values of every parameter, in the order the URL gave them. */\nexport type UrlState<TKey extends string> = Readonly<\n Record<TKey, readonly string[]>\n>;\n\nexport interface UrlQuery<TKey extends string, TContext> {\n /** Every parameter with no value: no filter at all. */\n readonly empty: UrlState<TKey>;\n parse(search: string): UrlState<TKey>;\n /** `''` for an empty state, else `?` and the parameters in declared order. */\n serialize(state: UrlState<TKey>): string;\n isEmpty(state: UrlState<TKey>): boolean;\n /** Whether any parameter that filters has a value; a sort alone does not. */\n isFiltering(state: UrlState<TKey>): boolean;\n /**\n * Whether the state asks the load anything: a filter, a sort or a page. A\n * selection alone does not.\n */\n isAsking(state: UrlState<TKey>): boolean;\n /**\n * The state with every filter emptied, its sort and selection kept, and its\n * page dropped.\n */\n withoutFilters(state: UrlState<TKey>): UrlState<TKey>;\n /**\n * The state with one parameter set to `values`, checked the way `parse`\n * checks them. Setting a filter or the sort also drops the page, because\n * it changes the answer the page was a page of.\n */\n withParam(\n state: UrlState<TKey>,\n key: TKey,\n values: readonly string[],\n ): UrlState<TKey>;\n /**\n * The load request the state stands for: one condition per parameter set,\n * the sort, and — when a page parameter is declared — `page` and `pageSize`.\n */\n request<TEntity extends Entity>(\n state: UrlState<TKey>,\n context: TContext,\n ): EntityLoadRequest<TEntity>;\n}\n\nexport interface UrlQueryOptions<TKey extends string, TContext> {\n readonly params: Readonly<Record<TKey, UrlParam<TContext>>>;\n /** Applied to every request, filtered or not, unless a sort is chosen. */\n readonly sorting?: EntitySorting<AnyEntity>[];\n}\n\n/**\n * A filter kept in the query string, read as an entifix `EntityLoadRequest`.\n *\n * entifix's own `parseLoadRequestParams` writes RSQL and accepts only members\n * marked `filterable`, which no collection or localized member may be. This\n * declares its parameters instead: readable URLs (`?tag=a&tag=b&q=text`) whose\n * every value is checked, each becoming one condition of the request.\n */\nexport function defineUrlQuery<TKey extends string, TContext = undefined>({\n params,\n sorting: defaultSorting,\n}: UrlQueryOptions<TKey, TContext>): UrlQuery<TKey, TContext> {\n const keys = Object.keys(params) as TKey[];\n const filterKeys = keys.filter(\n key =>\n !isSortParam(params[key]) &&\n !isSelectParam(params[key]) &&\n !isPageParam(params[key]),\n );\n const askingKeys = keys.filter(key => !isSelectParam(params[key]));\n const pageKeys = keys.filter(key => isPageParam(params[key]));\n // Two would be two answers to \"which page\", and the request has one.\n if (pageKeys.length > 1) {\n throw new Error(\n `A URL query declares one page parameter at most; found ${pageKeys.join(', ')}.`,\n );\n }\n const [pageKey] = pageKeys;\n const pageSize =\n pageKey === undefined ? undefined : (params[pageKey] as PageParam).pageSize;\n if (pageSize !== undefined && !(Number.isInteger(pageSize) && pageSize > 0)) {\n throw new Error(\n `The page parameter \"${pageKey}\" needs a positive integer pageSize; got ${pageSize}.`,\n );\n }\n const stateOf = (values: (key: TKey) => readonly string[]) =>\n Object.fromEntries(\n keys.map(key => [key, values(key)]),\n ) as unknown as UrlState<TKey>;\n const empty = stateOf(() => []);\n\n const accepted = (key: TKey, values: readonly string[]) => {\n const param = params[key];\n if (isPageParam(param)) {\n // Page 1 is the bare URL, so it is dropped rather than kept as a second\n // spelling of the same address.\n const [first] = values.filter(value => PAGE_NUMBER.test(value));\n return first === undefined || first === '1' ? [] : [first];\n }\n const { allowed, single } = isSortParam(param)\n ? { allowed: Object.keys(param.sorting), single: true }\n : isSelectParam(param)\n ? { allowed: param.allowed, single: true }\n : param;\n // Kept as written: a search box being typed into holds its spaces.\n const kept = values\n .filter(value => value.trim() !== '')\n .filter(value => allowed === undefined || allowed.includes(value));\n const unique = [...new Set(kept)];\n return single ? unique.slice(0, 1) : unique;\n };\n\n return {\n empty,\n parse(search) {\n const query = new URLSearchParams(search);\n return stateOf(key => accepted(key, query.getAll(key)));\n },\n serialize(state) {\n const query = new URLSearchParams();\n for (const key of keys) {\n for (const value of state[key]) query.append(key, value);\n }\n const text = query.toString();\n return text === '' ? '' : `?${text}`;\n },\n isEmpty(state) {\n return keys.every(key => state[key].length === 0);\n },\n isFiltering(state) {\n return filterKeys.some(key => state[key].length > 0);\n },\n isAsking(state) {\n return askingKeys.some(key => state[key].length > 0);\n },\n withoutFilters(state) {\n return stateOf(key =>\n filterKeys.includes(key) || key === pageKey ? [] : state[key],\n );\n },\n withParam(state, key, values) {\n const resetsPage = key !== pageKey && !isSelectParam(params[key]);\n return stateOf(other =>\n other === key\n ? accepted(key, values)\n : other === pageKey && resetsPage\n ? []\n : state[other],\n );\n },\n request<TEntity extends Entity>(state: UrlState<TKey>, context: TContext) {\n const filtering = filterKeys.flatMap(key => {\n const param = params[key] as FilterParam<TContext>;\n return state[key].length === 0\n ? []\n : [param.condition(state[key], context)];\n });\n const chosen = keys.flatMap(key => {\n const param = params[key];\n const [value] = state[key];\n return isSortParam(param) && value !== undefined\n ? [param.sorting[value] as EntitySorting<AnyEntity>[]]\n : [];\n });\n const sorting = chosen[0] ?? defaultSorting;\n const [page] = pageKey === undefined ? [] : state[pageKey];\n return {\n ...(pageSize !== undefined && {\n page: page === undefined ? 1 : Number(page),\n pageSize,\n }),\n ...(filtering.length > 0 && {\n filtering: filtering as unknown as EntityFiltering<TEntity>[],\n }),\n ...(sorting !== undefined && {\n sorting: sorting as unknown as EntitySorting<TEntity>[],\n }),\n };\n },\n };\n}\n\n/** Any of the values: `in`, which matches any element of a collection too. */\nexport function anyOf(property: string) {\n return (values: readonly string[]): Condition => ({\n property,\n operator: 'in',\n values: [...values],\n });\n}\n\n/**\n * The text as a substring of a member, as the repository's `like` matches\n * it. `property` names the member for a context, as a locale picks the\n * member holding its text.\n */\nexport function containing<TContext>(\n property: (context: TContext) => string,\n): FilterParam<TContext>['condition'] {\n return ([value], context) => ({\n property: property(context),\n operator: 'like',\n value: (value as string).trim(),\n });\n}\n\n/** A date inside any of the given years (UTC), as `between` per year. */\nexport function inAnyYear(property: string) {\n return (values: readonly string[]): Condition => ({\n operator: 'or',\n values: values.map(year => ({\n property,\n operator: 'between' as const,\n start: new Date(`${year}-01-01T00:00:00.000Z`),\n end: new Date(`${year}-12-31T23:59:59.999Z`),\n })),\n });\n}\n"],"names":["isSortParam","param","isPageParam","PAGE_NUMBER","isSelectParam","defineUrlQuery","params","sorting","defaultSorting","keys","Object","filterKeys","filter","key","askingKeys","pageKeys","length","Error","join","pageKey","pageSize","undefined","Number","isInteger","stateOf","values","fromEntries","map","empty","accepted","first","value","test","allowed","single","kept","trim","includes","unique","Set","slice","parse","search","query","URLSearchParams","getAll","serialize","state","append","text","toString","isEmpty","every","isFiltering","some","isAsking","withoutFilters","withParam","resetsPage","other","request","context","chosen","filtering","flatMap","condition","page","anyOf","property","operator","containing","inAnyYear","year","start","Date","end"],"mappings":"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"}
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  }
22
22
  }
23
23
  },
24
- "version": "0.8.0",
24
+ "version": "0.10.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": {
@@ -65,21 +65,21 @@
65
65
  "LICENSE"
66
66
  ],
67
67
  "dependencies": {
68
- "@entifix/business": "0.8.0",
69
- "@entifix/transactions": "0.8.0",
70
- "@entifix/core": "0.8.0"
68
+ "@entifix/core": "0.10.0",
69
+ "@entifix/business": "0.10.0",
70
+ "@entifix/transactions": "0.10.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@tanstack/react-form": "1.33.5",
74
74
  "@tanstack/react-query": "^5.104.5",
75
- "effect": "4.0.5",
75
+ "effect": "4.0.6",
76
76
  "react": "19.3.1",
77
- "@entifix/testing-unit": "0.8.0"
77
+ "@entifix/testing-unit": "0.10.0"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@tanstack/react-form": ">=1.33 <2",
81
81
  "@tanstack/react-query": ">=5.101 <6",
82
- "effect": "^4.0.5",
82
+ "effect": "^4.0.6",
83
83
  "react": ">=19 <20"
84
84
  }
85
85
  }