@entifix/react-integration 0.1.0-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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/context/adapters-context/adapters-context.d.ts +4 -0
  4. package/dist/context/adapters-context/adapters-context.d.ts.map +1 -0
  5. package/dist/context/adapters-context/adapters-context.js +19 -0
  6. package/dist/context/adapters-context/adapters-context.js.map +1 -0
  7. package/dist/context/adapters-context/index.d.ts +2 -0
  8. package/dist/context/adapters-context/index.d.ts.map +1 -0
  9. package/dist/context/adapters-context/index.js +3 -0
  10. package/dist/context/adapters-context/index.js.map +1 -0
  11. package/dist/hooks/use-data-loading/index.d.ts +3 -0
  12. package/dist/hooks/use-data-loading/index.d.ts.map +1 -0
  13. package/dist/hooks/use-data-loading/index.js +4 -0
  14. package/dist/hooks/use-data-loading/index.js.map +1 -0
  15. package/dist/hooks/use-data-loading/use-data-loading.d.ts +26 -0
  16. package/dist/hooks/use-data-loading/use-data-loading.d.ts.map +1 -0
  17. package/dist/hooks/use-data-loading/use-data-loading.js +167 -0
  18. package/dist/hooks/use-data-loading/use-data-loading.js.map +1 -0
  19. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts +53 -0
  20. package/dist/hooks/use-data-loading/use-data-loading.types.d.ts.map +1 -0
  21. package/dist/hooks/use-data-loading/use-data-loading.types.js +3 -0
  22. package/dist/hooks/use-data-loading/use-data-loading.types.js.map +1 -0
  23. package/dist/hooks/use-entity-form/index.d.ts +4 -0
  24. package/dist/hooks/use-entity-form/index.d.ts.map +1 -0
  25. package/dist/hooks/use-entity-form/index.js +5 -0
  26. package/dist/hooks/use-entity-form/index.js.map +1 -0
  27. package/dist/hooks/use-entity-form/use-entity-form.d.ts +31 -0
  28. package/dist/hooks/use-entity-form/use-entity-form.d.ts.map +1 -0
  29. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +136 -0
  30. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -0
  31. package/dist/hooks/use-entity-form/use-entity-form.helpers.js +337 -0
  32. package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -0
  33. package/dist/hooks/use-entity-form/use-entity-form.js +209 -0
  34. package/dist/hooks/use-entity-form/use-entity-form.js.map +1 -0
  35. package/dist/hooks/use-entity-form/use-entity-form.types.d.ts +173 -0
  36. package/dist/hooks/use-entity-form/use-entity-form.types.d.ts.map +1 -0
  37. package/dist/hooks/use-entity-form/use-entity-form.types.js +3 -0
  38. package/dist/hooks/use-entity-form/use-entity-form.types.js.map +1 -0
  39. package/dist/hooks/use-entity-link-resolver/index.d.ts +2 -0
  40. package/dist/hooks/use-entity-link-resolver/index.d.ts.map +1 -0
  41. package/dist/hooks/use-entity-link-resolver/index.js +3 -0
  42. package/dist/hooks/use-entity-link-resolver/index.js.map +1 -0
  43. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts +17 -0
  44. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts.map +1 -0
  45. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js +26 -0
  46. package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js.map +1 -0
  47. package/dist/hooks/use-entity-link-source/index.d.ts +3 -0
  48. package/dist/hooks/use-entity-link-source/index.d.ts.map +1 -0
  49. package/dist/hooks/use-entity-link-source/index.js +4 -0
  50. package/dist/hooks/use-entity-link-source/index.js.map +1 -0
  51. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts +25 -0
  52. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -0
  53. package/dist/hooks/use-entity-link-source/use-entity-link-source.js +169 -0
  54. package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -0
  55. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts +49 -0
  56. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts.map +1 -0
  57. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js +3 -0
  58. package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js.map +1 -0
  59. package/dist/hooks/use-entity-mutation/index.d.ts +3 -0
  60. package/dist/hooks/use-entity-mutation/index.d.ts.map +1 -0
  61. package/dist/hooks/use-entity-mutation/index.js +4 -0
  62. package/dist/hooks/use-entity-mutation/index.js.map +1 -0
  63. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts +22 -0
  64. package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts.map +1 -0
  65. package/dist/hooks/use-entity-mutation/use-entity-mutation.js +88 -0
  66. package/dist/hooks/use-entity-mutation/use-entity-mutation.js.map +1 -0
  67. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts +19 -0
  68. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts.map +1 -0
  69. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js +3 -0
  70. package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js.map +1 -0
  71. package/dist/hooks/use-entity-record/index.d.ts +3 -0
  72. package/dist/hooks/use-entity-record/index.d.ts.map +1 -0
  73. package/dist/hooks/use-entity-record/index.js +4 -0
  74. package/dist/hooks/use-entity-record/index.js.map +1 -0
  75. package/dist/hooks/use-entity-record/use-entity-record.d.ts +21 -0
  76. package/dist/hooks/use-entity-record/use-entity-record.d.ts.map +1 -0
  77. package/dist/hooks/use-entity-record/use-entity-record.js +93 -0
  78. package/dist/hooks/use-entity-record/use-entity-record.js.map +1 -0
  79. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts +22 -0
  80. package/dist/hooks/use-entity-record/use-entity-record.types.d.ts.map +1 -0
  81. package/dist/hooks/use-entity-record/use-entity-record.types.js +3 -0
  82. package/dist/hooks/use-entity-record/use-entity-record.types.js.map +1 -0
  83. package/dist/hooks/use-entity-use-cases/index.d.ts +2 -0
  84. package/dist/hooks/use-entity-use-cases/index.d.ts.map +1 -0
  85. package/dist/hooks/use-entity-use-cases/index.js +3 -0
  86. package/dist/hooks/use-entity-use-cases/index.js.map +1 -0
  87. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts +25 -0
  88. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts.map +1 -0
  89. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js +47 -0
  90. package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js.map +1 -0
  91. package/dist/hooks/use-wizard/index.d.ts +3 -0
  92. package/dist/hooks/use-wizard/index.d.ts.map +1 -0
  93. package/dist/hooks/use-wizard/index.js +4 -0
  94. package/dist/hooks/use-wizard/index.js.map +1 -0
  95. package/dist/hooks/use-wizard/use-wizard.d.ts +19 -0
  96. package/dist/hooks/use-wizard/use-wizard.d.ts.map +1 -0
  97. package/dist/hooks/use-wizard/use-wizard.js +136 -0
  98. package/dist/hooks/use-wizard/use-wizard.js.map +1 -0
  99. package/dist/hooks/use-wizard/use-wizard.types.d.ts +87 -0
  100. package/dist/hooks/use-wizard/use-wizard.types.d.ts.map +1 -0
  101. package/dist/hooks/use-wizard/use-wizard.types.js +3 -0
  102. package/dist/hooks/use-wizard/use-wizard.types.js.map +1 -0
  103. package/dist/index.d.ts +17 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +18 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/query/combine-filtering.d.ts +11 -0
  108. package/dist/query/combine-filtering.d.ts.map +1 -0
  109. package/dist/query/combine-filtering.js +18 -0
  110. package/dist/query/combine-filtering.js.map +1 -0
  111. package/dist/query/entity-query-key.d.ts +28 -0
  112. package/dist/query/entity-query-key.d.ts.map +1 -0
  113. package/dist/query/entity-query-key.js +28 -0
  114. package/dist/query/entity-query-key.js.map +1 -0
  115. package/dist/query/query-provider.d.ts +21 -0
  116. package/dist/query/query-provider.d.ts.map +1 -0
  117. package/dist/query/query-provider.js +35 -0
  118. package/dist/query/query-provider.js.map +1 -0
  119. package/dist/reactive/event-source-channel.d.ts +22 -0
  120. package/dist/reactive/event-source-channel.d.ts.map +1 -0
  121. package/dist/reactive/event-source-channel.js +93 -0
  122. package/dist/reactive/event-source-channel.js.map +1 -0
  123. package/dist/reactive/reactive-channel.d.ts +75 -0
  124. package/dist/reactive/reactive-channel.d.ts.map +1 -0
  125. package/dist/reactive/reactive-channel.js +55 -0
  126. package/dist/reactive/reactive-channel.js.map +1 -0
  127. package/dist/reactive/use-reactive-invalidation.d.ts +10 -0
  128. package/dist/reactive/use-reactive-invalidation.d.ts.map +1 -0
  129. package/dist/reactive/use-reactive-invalidation.js +25 -0
  130. package/dist/reactive/use-reactive-invalidation.js.map +1 -0
  131. package/dist/reactive/use-transaction-settlement.d.ts +41 -0
  132. package/dist/reactive/use-transaction-settlement.d.ts.map +1 -0
  133. package/dist/reactive/use-transaction-settlement.js +123 -0
  134. package/dist/reactive/use-transaction-settlement.js.map +1 -0
  135. package/package.json +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 r10c Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @entifix/react-integration
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Running unit tests
6
+
7
+ Run `nx test @entifix/react-integration` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,4 @@
1
+ import { type Context } from 'react';
2
+ export declare function createAdaptersContext<TAdapters>(): Context<TAdapters>;
3
+ export declare function useAdaptersContext<TAdapters>(context: Context<TAdapters>): TAdapters;
4
+ //# sourceMappingURL=adapters-context.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,19 @@
1
+ import { EntifixBuildError } from '@entifix/core';
2
+ import { createContext, useContext } from 'react';
3
+ export function createAdaptersContext() {
4
+ return /*#__PURE__*/ createContext({});
5
+ }
6
+ export function useAdaptersContext(context) {
7
+ const contextValue = useContext(context);
8
+ // The default `createAdaptersContext` hands out is `{}`, so "nobody mounted
9
+ // a provider" and "the object has no keys" are the same question. Asked
10
+ // directly rather than through a generic `isEmpty`: that helper also answers
11
+ // for strings, arrays and null, none of which can reach here, and it was the
12
+ // single import crossing out of entifix.
13
+ 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.');
15
+ }
16
+ return contextValue;
17
+ }
18
+
19
+ //# sourceMappingURL=adapters-context.js.map
@@ -0,0 +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"}
@@ -0,0 +1,2 @@
1
+ export * from './adapters-context.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/context/adapters-context/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './adapters-context.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/context/adapters-context/index.ts"],"sourcesContent":["export * from './adapters-context.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB"}
@@ -0,0 +1,3 @@
1
+ export * from './use-data-loading.js';
2
+ export * from './use-data-loading.types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './use-data-loading.js';
2
+ export * from './use-data-loading.types.js';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/use-data-loading/index.ts"],"sourcesContent":["export * from './use-data-loading.js';\nexport * from './use-data-loading.types.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB;AACtC,cAAc,8BAA8B"}
@@ -0,0 +1,26 @@
1
+ import { type EntifixError, type Entity, type EntitySorting, type FilterGroup } from '@entifix/core';
2
+ import type { UseDataLoadingOptions } from './use-data-loading.types.js';
3
+ /**
4
+ * Runs an entity list use-case and caches the result through TanStack Query.
5
+ *
6
+ * The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the
7
+ * *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations
8
+ * and reactive events invalidate the list. The public shape is unchanged: the
9
+ * `uc`/`ctx` come in the same way and the Effect runs against the same provided
10
+ * Tags, so callers and organisms are untouched.
11
+ */
12
+ export declare function useDataLoading<TEntity extends Entity, TContext>({ uc, ctx, initialPageSize, queryKey, enabled, baseFiltering, }: UseDataLoadingOptions<TEntity, TContext>): {
13
+ isLoading: boolean;
14
+ items: TEntity[];
15
+ totalItems: number;
16
+ currentPage: number;
17
+ pageSize: number;
18
+ error: EntifixError | undefined;
19
+ filtering: FilterGroup<TEntity> | undefined;
20
+ sorting: EntitySorting<TEntity> | undefined;
21
+ onPageChange: (newPage: number) => void;
22
+ onPageSizeChange: (newPageSize: number) => void;
23
+ onFilteringChange: (filtering: FilterGroup<TEntity>) => void;
24
+ onSortingChange: (sorting: EntitySorting<TEntity>) => void;
25
+ };
26
+ //# sourceMappingURL=use-data-loading.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,167 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { EntityLoadRequestTag } from '@entifix/business';
3
+ import { serializeRsql, serializeSort } from '@entifix/core';
4
+ import { keepPreviousData, useQuery } from '@tanstack/react-query';
5
+ import { Context, Effect } from 'effect';
6
+ import { useCallback, useEffect, useId, useMemo, useReducer, useRef } from 'react';
7
+ import { combineFilterGroups } from '../../query/combine-filtering.js';
8
+ const DEFAULT_PAGE_SIZE = 10;
9
+ function requestReducer(state, action) {
10
+ return _extends({}, state, action.set);
11
+ }
12
+ function initialRequest(pageSize) {
13
+ return {
14
+ currentPage: 1,
15
+ pageSize,
16
+ filtering: undefined,
17
+ sorting: undefined
18
+ };
19
+ }
20
+ /**
21
+ * Runs an entity list use-case and caches the result through TanStack Query.
22
+ *
23
+ * The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the
24
+ * *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations
25
+ * and reactive events invalidate the list. The public shape is unchanged: the
26
+ * `uc`/`ctx` come in the same way and the Effect runs against the same provided
27
+ * Tags, so callers and organisms are untouched.
28
+ */ export function useDataLoading({ uc, ctx, initialPageSize = DEFAULT_PAGE_SIZE, queryKey, enabled = true, baseFiltering }) {
29
+ var _ref, _ref1, _query_error;
30
+ var _query_data, _query_data1;
31
+ const [request, dispatch] = useReducer(requestReducer, initialPageSize, initialRequest);
32
+ // Callers rebuild `uc`/`ctx` inline every render, so hold the latest in refs
33
+ // and keep them out of the query key — keying on their identity would refetch
34
+ // on every render. Written in an effect (refs may not be set during render);
35
+ // effects fire in order, so the query below always reads the latest.
36
+ const ucRef = useRef(uc);
37
+ const ctxRef = useRef(ctx);
38
+ useEffect(()=>{
39
+ ucRef.current = uc;
40
+ ctxRef.current = ctx;
41
+ });
42
+ // What actually goes on the wire: the caller's standing restriction ANDed with
43
+ // whatever the user applied. Kept separate from `request.filtering` because the
44
+ // restriction is not the user's filter — the panel must keep showing only what
45
+ // they set, while the request keeps carrying both.
46
+ const effectiveFiltering = useMemo(()=>combineFilterGroups(baseFiltering, request.filtering), [
47
+ baseFiltering,
48
+ request.filtering
49
+ ]);
50
+ // The applied filter/sort are rebuilt each render, so their *serialized* form
51
+ // — the same codec the request needs anyway — is what keys the query: a stable
52
+ // string that changes exactly when the query does.
53
+ const rsql = useMemo(()=>serializeRsql(effectiveFiltering ? [
54
+ effectiveFiltering
55
+ ] : undefined), [
56
+ effectiveFiltering
57
+ ]);
58
+ const sort = useMemo(()=>serializeSort(request.sorting ? [
59
+ request.sorting
60
+ ] : undefined), [
61
+ request.sorting
62
+ ]);
63
+ const loadRequest = useMemo(()=>{
64
+ const built = {
65
+ page: request.currentPage,
66
+ pageSize: request.pageSize
67
+ };
68
+ // An empty group serializes to nothing; omit it rather than send a filter
69
+ // that matches everything.
70
+ if (rsql !== '' && effectiveFiltering) {
71
+ built.filtering = [
72
+ effectiveFiltering
73
+ ];
74
+ }
75
+ if (sort !== '' && request.sorting) {
76
+ built.sorting = [
77
+ request.sorting
78
+ ];
79
+ }
80
+ return built;
81
+ }, [
82
+ request.currentPage,
83
+ request.pageSize,
84
+ effectiveFiltering,
85
+ request.sorting,
86
+ rsql,
87
+ sort
88
+ ]);
89
+ // Falls back to a per-instance id so an un-scoped list never collides with
90
+ // another in the shared cache — correct, just unshared (see the type doc).
91
+ const instanceId = useId();
92
+ const scope = queryKey != null ? queryKey : [
93
+ instanceId
94
+ ];
95
+ const query = useQuery({
96
+ enabled,
97
+ /**
98
+ * Page, size, filter and sort are all part of the key, so every one of them
99
+ * is a *different* query — and without this, `data` goes undefined the
100
+ * instant any of them changes. The table would then see `items.length === 0`
101
+ * with `isFetching` true, read it as a first load, and blank populated rows
102
+ * to skeleton on every pagination click: the grey flash the loading contract
103
+ * exists to prevent. Keeping the previous page is what makes "skeleton only
104
+ * on first load" true rather than merely intended.
105
+ */ placeholderData: keepPreviousData,
106
+ queryKey: [
107
+ ...scope,
108
+ 'load',
109
+ request.currentPage,
110
+ request.pageSize,
111
+ rsql,
112
+ sort
113
+ ],
114
+ queryFn: ()=>Effect.runPromise(Effect.provide(ucRef.current, ctxRef.current.pipe(Context.add(EntityLoadRequestTag, // The tag carries no generic, so the entity-typed request crosses
115
+ // it — the same cast `loadUCFactory` makes reading it back.
116
+ loadRequest))))
117
+ });
118
+ const onPageChange = useCallback((newPage)=>{
119
+ dispatch({
120
+ set: {
121
+ currentPage: newPage
122
+ }
123
+ });
124
+ }, []);
125
+ const onPageSizeChange = useCallback((newPageSize)=>{
126
+ dispatch({
127
+ set: {
128
+ pageSize: newPageSize,
129
+ currentPage: 1
130
+ }
131
+ });
132
+ }, []);
133
+ // Narrowing the result set invalidates the page number: page 4 of the old
134
+ // result is very likely past the end of the new one.
135
+ const onFilteringChange = useCallback((filtering)=>{
136
+ dispatch({
137
+ set: {
138
+ filtering,
139
+ currentPage: 1
140
+ }
141
+ });
142
+ }, []);
143
+ const onSortingChange = useCallback((sorting)=>{
144
+ dispatch({
145
+ set: {
146
+ sorting,
147
+ currentPage: 1
148
+ }
149
+ });
150
+ }, []);
151
+ return {
152
+ isLoading: query.isFetching,
153
+ items: (_ref = (_query_data = query.data) == null ? void 0 : _query_data.items) != null ? _ref : [],
154
+ totalItems: (_ref1 = (_query_data1 = query.data) == null ? void 0 : _query_data1.total) != null ? _ref1 : 0,
155
+ currentPage: request.currentPage,
156
+ pageSize: request.pageSize,
157
+ error: (_query_error = query.error) != null ? _query_error : undefined,
158
+ filtering: request.filtering,
159
+ sorting: request.sorting,
160
+ onPageChange,
161
+ onPageSizeChange,
162
+ onFilteringChange,
163
+ onSortingChange
164
+ };
165
+ }
166
+
167
+ //# sourceMappingURL=use-data-loading.js.map
@@ -0,0 +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":"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"}
@@ -0,0 +1,53 @@
1
+ import { EntityLoadRequestTag } from '@entifix/business';
2
+ import { EntifixError, Entity, EntityPage, EntitySorting, FilterGroup } from '@entifix/core';
3
+ import { Context, Effect } from 'effect';
4
+ export interface UseDataLoadingState<T extends Entity> {
5
+ isLoading: boolean;
6
+ items: Array<T>;
7
+ totalItems: number;
8
+ currentPage: number;
9
+ pageSize: number;
10
+ error: EntifixError | undefined;
11
+ /**
12
+ * The filtering and sorting currently *applied* — not a draft. The controls
13
+ * commit through `onFilteringChange`/`onSortingChange`, so every value here
14
+ * has already been requested from the server.
15
+ */
16
+ filtering: FilterGroup<T> | undefined;
17
+ sorting: EntitySorting<T> | undefined;
18
+ }
19
+ export type UseDataLoadingAction<T extends Entity> = {
20
+ set: Partial<UseDataLoadingState<T>>;
21
+ };
22
+ export interface UseDataLoadingOptions<TEntity extends Entity, TContext> {
23
+ uc: Effect.Effect<EntityPage<TEntity>, EntifixError, EntityLoadRequestTag | TContext>;
24
+ ctx: Context.Context<TContext>;
25
+ /**
26
+ * Page size for the first load; defaults to 10. Callers that need the whole
27
+ * set rather than a page — a relation picker, say — raise this.
28
+ */
29
+ initialPageSize?: number;
30
+ /**
31
+ * The stable prefix that scopes this list in the shared query cache — pass
32
+ * {@link entityQueryScope}`(Ctor)` so refetches, optimistic writes, and
33
+ * reactive invalidation of the same entity all address one cache entry.
34
+ * Omit it and the hook falls back to a per-instance id: correct in isolation
35
+ * (no cross-list collisions) but unshared, so nothing outside this hook can
36
+ * invalidate it.
37
+ */
38
+ queryKey?: ReadonlyArray<unknown>;
39
+ /**
40
+ * `false` holds the request back without unmounting the hook — what a picker's
41
+ * browse dialog needs, since a list nobody has opened yet must not be fetched.
42
+ * Defaults to `true`, so every existing caller loads on mount as before.
43
+ */
44
+ enabled?: boolean;
45
+ /**
46
+ * A standing restriction ANDed into every request and never shown in the filter
47
+ * panel — "these are the rows this caller may see at all", as opposed to the
48
+ * filter the user is editing. A link picker narrowing what may be assigned uses
49
+ * it; so would a listing scoped to one tenant.
50
+ */
51
+ baseFiltering?: FilterGroup<TEntity>;
52
+ }
53
+ //# sourceMappingURL=use-data-loading.types.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=use-data-loading.types.js.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ export * from './use-entity-form.helpers.js';
2
+ export * from './use-entity-form.js';
3
+ export * from './use-entity-form.types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './use-entity-form.helpers.js';
2
+ export * from './use-entity-form.js';
3
+ export * from './use-entity-form.types.js';
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/index.ts"],"sourcesContent":["export * from './use-entity-form.helpers.js';\nexport * from './use-entity-form.js';\nexport * from './use-entity-form.types.js';\n"],"names":[],"mappings":"AAAA,cAAc,+BAA+B;AAC7C,cAAc,uBAAuB;AACrC,cAAc,6BAA6B"}
@@ -0,0 +1,31 @@
1
+ import { type Entity } from '@entifix/core';
2
+ import type { UseEntityFormOptions, UseEntityFormResult } from './use-entity-form.types.js';
3
+ /**
4
+ * The write half of an entity form: holds the string draft and reports
5
+ * metadata-derived validation, the counterpart to {@link useEntityRecord}
6
+ * (which reads the record it seeds from).
7
+ *
8
+ * It stays out of the transport: it neither fetches nor saves. `onSubmit`
9
+ * receives the validated draft, and the per-entity wrapper reconstructs the
10
+ * domain entity and calls the mutation. The result plugs straight into the
11
+ * agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).
12
+ *
13
+ * TanStack Form owns the draft and the submission lifecycle; that is an
14
+ * implementation detail, and the returned shape is deliberately the plain
15
+ * `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which
16
+ * renders native inputs and knows nothing about any form library — stays
17
+ * agnostic. Fields are therefore never registered through `form.Field`; a
18
+ * form-level validator reporting `{ fields }` reaches them anyway, because
19
+ * `FormApi` creates meta for any field name an error arrives for.
20
+ *
21
+ * `revalidateLogic` is what makes a pristine form quiet: nothing validates until
22
+ * the first submit, and every keystroke revalidates after it. Running one
23
+ * validator under a single cause also keeps a fixed field from holding a stale
24
+ * message left behind by another cause.
25
+ *
26
+ * The seed applies once, so a caller that loads its record after mount keys the
27
+ * form by the record id to reseed — the same convention `EntityForm`'s wrappers
28
+ * already follow.
29
+ */
30
+ export declare function useEntityForm<TEntity extends Entity>({ entityConstructor, entity, draft, fields, schema, validate, validationMessages, translateKey, onSubmit, }: UseEntityFormOptions<TEntity>): UseEntityFormResult;
31
+ //# sourceMappingURL=use-entity-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-entity-form.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAIZ,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,iBAAiB,EACjB,MAAM,EACN,KAAK,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,QAAQ,GACT,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,mBAAmB,CAyLrD"}
@@ -0,0 +1,136 @@
1
+ import { type EntityDraft, type EntityDraftValue, type EntityFieldDescriptor, type StandardSchemaV1 } from '@entifix/core';
2
+ /**
3
+ * The value a field seeds with from a record.
4
+ *
5
+ * A scalar seeds as a **string**: links with their foreign key(s), dates with a
6
+ * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.
7
+ *
8
+ * The array branch is explicit rather than left to `String(raw)`, even though
9
+ * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by
10
+ * way of `Array.prototype.toString`, which is an accident: it was doing the
11
+ * work of a `scalarCollection` seed before the type existed, and
12
+ * `reconstructEntity` was handing the same string straight back as a `string`.
13
+ * Both halves were wrong in the same direction, so the fixed-point spec could
14
+ * not see it. Now the join is declared here and the split is declared there,
15
+ * and they are inverses on purpose.
16
+ *
17
+ * A **`composition`** is the one member that does not seed as a string, because
18
+ * it has no lossless string form at all — its rows seed as row drafts, each
19
+ * with a freshly minted key. That key is the only thing here the record does not
20
+ * supply, and it is minted at seed time rather than at render so that the rows a
21
+ * form starts with are already addressable.
22
+ */
23
+ export declare function seedFieldValue(descriptor: EntityFieldDescriptor, entity: unknown): EntityDraftValue;
24
+ /** Builds the initial draft for a record (or an empty one for a create). */
25
+ export declare function seedEntityDraft(descriptors: readonly EntityFieldDescriptor[], entity: unknown): EntityDraft;
26
+ /**
27
+ * Restores a persisted draft **over** a freshly seeded one, rather than in place
28
+ * of it.
29
+ *
30
+ * A draft outlives the entity that wrote it — it sits in IndexedDB across
31
+ * refreshes and deployments — so the two can disagree by the time it comes back.
32
+ * Replacing the seed outright is how that disagreement becomes a bug the user
33
+ * sees: a member added since the draft was written has no entry, reaches the form
34
+ * engine as `undefined`, and its input flips from controlled to uncontrolled
35
+ * mid-render. A member since removed goes the other way, surviving as a value
36
+ * nothing renders and `reconstructEntity` cannot assign.
37
+ *
38
+ * So the seed decides the *keys* and the draft decides the *values*: a name the
39
+ * entity no longer declares is dropped, and a name it declares that the draft
40
+ * lacks keeps its seeded value.
41
+ *
42
+ * **A value is restored only in the shape its member can hold**, which is now
43
+ * two shapes rather than one. A scalar takes a string; a `composition` takes a
44
+ * readable row list. Anything else is dropped back to its seed — a member that
45
+ * changed type between the write and the read, or a row list from a build that
46
+ * did not carry keys, restores as the record's own value instead of as
47
+ * something no editor can render. This is per member, so one unreadable entry
48
+ * never costs the user the rest of the form.
49
+ *
50
+ * This is the shape guard at the one place the shape is actually known. The
51
+ * store-level version check (`drafts-state.ts`) can only decide that a whole
52
+ * generation of drafts is unreadable; it cannot know which members one entity
53
+ * declares.
54
+ */
55
+ export declare function restoreEntityDraft(descriptors: readonly EntityFieldDescriptor[], seed: EntityDraft, persisted: EntityDraft | undefined): EntityDraft;
56
+ /** The slice of a form field's state this module reads: its error list. */
57
+ export interface FieldErrorMeta {
58
+ errors?: readonly unknown[];
59
+ }
60
+ /**
61
+ * Flattens the form engine's per-field state into the `name → message` map the
62
+ * agnostic `EntityForm` renders. Only the first message per field survives: a
63
+ * row shows one line, and the metadata rules already stop at the first failure.
64
+ *
65
+ * Kept structural rather than typed against the engine so the rest of this
66
+ * module stays independent of which form library backs the hook.
67
+ */
68
+ export declare function readFieldErrors(fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>): Record<string, string>;
69
+ /**
70
+ * The four metadata-derived messages, already localized. Taken as an argument
71
+ * rather than built here so this stays a pure function — and because the
72
+ * sentence cannot be assembled from a label plus a hardcoded suffix: word order
73
+ * differs between locales.
74
+ */
75
+ export interface EntityDraftMessages {
76
+ required(field: string): string;
77
+ number(field: string): string;
78
+ date(field: string): string;
79
+ option(field: string): string;
80
+ }
81
+ /**
82
+ * Validates a draft against what the metadata implies — `required` members must
83
+ * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only
84
+ * members are skipped entirely; a relation is checked for presence but never
85
+ * for format, since its draft value is a foreign key this side cannot resolve.
86
+ *
87
+ * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}
88
+ * is what layers a schema and the caller's own rules over this.
89
+ */
90
+ export declare function validateEntityDraft(descriptors: readonly EntityFieldDescriptor[], values: EntityDraft, messages: EntityDraftMessages): Record<string, string>;
91
+ /**
92
+ * Runs a Standard Schema over the draft and sorts its issues into per-field
93
+ * messages plus a form-level one (an issue with no path — a cross-field rule).
94
+ *
95
+ * **Schemas are authored against the string draft**, so they coerce
96
+ * (`z.coerce.number()`) rather than expect typed values: the draft is what the
97
+ * inputs round-trip, and the same schema then validates exactly what the user
98
+ * sees. Only the first issue per field survives, matching how a row renders.
99
+ *
100
+ * Synchronous rules only. An async schema returns a Promise the form's
101
+ * synchronous validation pass cannot wait on, so it is rejected loudly rather
102
+ * than silently passing.
103
+ */
104
+ export declare function readSchemaIssues(schema: StandardSchemaV1, values: EntityDraft, translateIssue: (message: string, field: string | undefined) => string): {
105
+ fields: Record<string, string>;
106
+ form?: string;
107
+ };
108
+ /** Everything {@link composeEntityFormErrors} needs to judge one draft. */
109
+ export interface ComposeEntityFormErrorsOptions {
110
+ descriptors: readonly EntityFieldDescriptor[];
111
+ values: EntityDraft;
112
+ messages: EntityDraftMessages;
113
+ schema?: StandardSchemaV1;
114
+ translateIssue: (message: string, field: string | undefined) => string;
115
+ validate?: (values: EntityDraft) => Record<string, string>;
116
+ }
117
+ /**
118
+ * The single composition point for a draft's errors: metadata rules first, then
119
+ * the entity's schema, then the caller's `validate` callback. Later sources win
120
+ * on conflict, so a caller can always override a message it disagrees with.
121
+ */
122
+ export declare function composeEntityFormErrors({ descriptors, values, messages, schema, translateIssue, validate, }: ComposeEntityFormErrorsOptions): {
123
+ fields: Record<string, string>;
124
+ form?: string;
125
+ };
126
+ /**
127
+ * What a caller who supplies no {@link ValidationMessages} gets: the catalog key
128
+ * itself, with the field appended.
129
+ *
130
+ * Honest rather than English — there is no catalog in this package to resolve
131
+ * against — and visible enough in a form that a host notices it has wired no
132
+ * copy. It sits beside the validator rather than in `.types.ts`, which this
133
+ * repository keeps type-only.
134
+ */
135
+ export declare const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages;
136
+ //# sourceMappingURL=use-entity-form.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAU1B,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAwCvB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,OAAO,GACd,gBAAgB,CAKlB;AAqDD,4EAA4E;AAC5E,wBAAgB,eAAe,CAC7B,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,OAAO,GACd,WAAW,CAMb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,WAAW,GAAG,SAAS,GACjC,WAAW,CAeb;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAkHD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GACrE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBnD;AAED,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,QAAQ,GACT,EAAE,8BAA8B,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAUA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAKzC,CAAC"}