@kozou/ui-core 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +40 -0
  3. package/dist/adapter/errors.d.ts +16 -0
  4. package/dist/adapter/errors.d.ts.map +1 -0
  5. package/dist/adapter/errors.js +19 -0
  6. package/dist/adapter/errors.js.map +1 -0
  7. package/dist/adapter/index.d.ts +8 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/index.js +15 -0
  10. package/dist/adapter/index.js.map +1 -0
  11. package/dist/adapter/kozou-api.d.ts +35 -0
  12. package/dist/adapter/kozou-api.d.ts.map +1 -0
  13. package/dist/adapter/kozou-api.js +215 -0
  14. package/dist/adapter/kozou-api.js.map +1 -0
  15. package/dist/adapter/postgrest.d.ts +51 -0
  16. package/dist/adapter/postgrest.d.ts.map +1 -0
  17. package/dist/adapter/postgrest.js +343 -0
  18. package/dist/adapter/postgrest.js.map +1 -0
  19. package/dist/adapter/types.d.ts +6 -0
  20. package/dist/adapter/types.d.ts.map +1 -0
  21. package/dist/adapter/types.js +3 -0
  22. package/dist/adapter/types.js.map +1 -0
  23. package/dist/detail/format-cell.d.ts +7 -0
  24. package/dist/detail/format-cell.d.ts.map +1 -0
  25. package/dist/detail/format-cell.js +50 -0
  26. package/dist/detail/format-cell.js.map +1 -0
  27. package/dist/detail/resolve-fk-labels.d.ts +23 -0
  28. package/dist/detail/resolve-fk-labels.d.ts.map +1 -0
  29. package/dist/detail/resolve-fk-labels.js +118 -0
  30. package/dist/detail/resolve-fk-labels.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/list/list-href.d.ts +21 -0
  36. package/dist/list/list-href.d.ts.map +1 -0
  37. package/dist/list/list-href.js +50 -0
  38. package/dist/list/list-href.js.map +1 -0
  39. package/dist/query/list-params.d.ts +18 -0
  40. package/dist/query/list-params.d.ts.map +1 -0
  41. package/dist/query/list-params.js +55 -0
  42. package/dist/query/list-params.js.map +1 -0
  43. package/dist/resource-id.d.ts +24 -0
  44. package/dist/resource-id.d.ts.map +1 -0
  45. package/dist/resource-id.js +58 -0
  46. package/dist/resource-id.js.map +1 -0
  47. package/dist/server/fk-row-cache.d.ts +27 -0
  48. package/dist/server/fk-row-cache.d.ts.map +1 -0
  49. package/dist/server/fk-row-cache.js +54 -0
  50. package/dist/server/fk-row-cache.js.map +1 -0
  51. package/dist/server/schema-cache.d.ts +22 -0
  52. package/dist/server/schema-cache.d.ts.map +1 -0
  53. package/dist/server/schema-cache.js +46 -0
  54. package/dist/server/schema-cache.js.map +1 -0
  55. package/dist/view/columns.d.ts +4 -0
  56. package/dist/view/columns.d.ts.map +1 -0
  57. package/dist/view/columns.js +22 -0
  58. package/dist/view/columns.js.map +1 -0
  59. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ // @kozou/ui-core — framework-agnostic UI logic shared by reference UIs.
2
+ //
3
+ // This is the read-path slice extracted from @kozou/svelte-ui. None of
4
+ // the modules below import Svelte / SvelteKit / React or any other UI
5
+ // framework runtime; they turn a SchemaContext + DataAdapter into the
6
+ // data a list/detail view renders. The reference Svelte UI consumes
7
+ // them, and any additional UI (e.g. a React renderer) can consume the
8
+ // same logic instead of re-implementing it.
9
+ //
10
+ // DataAdapter / ResourceId / ListParams / ListResult / RelationOption
11
+ // and the SchemaContext shape itself live in @kozou/core; they are not
12
+ // re-exported here.
13
+ export { AdapterError, PostgrestAdapterError, PostgrestDataAdapter, KozouApiAdapterError, KozouApiDataAdapter, } from './adapter/index.js';
14
+ // Resource id: composite-key segment encode / decode / parse.
15
+ export { rowIdSegment, encodeResourceId, parseResourceId, } from './resource-id.js';
16
+ // List params: URL <-> ListParams.
17
+ export { DEFAULT_PAGE_SIZE, parseListParamsFromUrl, } from './query/list-params.js';
18
+ // List href helpers + list-cell formatting.
19
+ export { buildHref, buildSortHref, formatCell } from './list/list-href.js';
20
+ // View column heuristics.
21
+ export { pickViewDisplayColumns, pickViewSearchFields, } from './view/columns.js';
22
+ // Detail cell formatting + FK label resolution.
23
+ export { formatCellValue } from './detail/format-cell.js';
24
+ export { resolveFkLabels } from './detail/resolve-fk-labels.js';
25
+ // Server-side caches (clock/loader injected; no Node-only dependency).
26
+ export { SchemaCache } from './server/schema-cache.js';
27
+ export { FkRowCache } from './server/fk-row-cache.js';
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,sEAAsE;AACtE,oEAAoE;AACpE,sEAAsE;AACtE,4CAA4C;AAC5C,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,oBAAoB;AAWpB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,8DAA8D;AAC9D,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAE1B,mCAAmC;AACnC,OAAO,EACL,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAMhC,4CAA4C;AAC5C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG3E,0BAA0B;AAC1B,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,gDAAgD;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAMhE,uEAAuE;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAMvD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { SortSpec } from '@kozou/core';
2
+ /** The client-visible slice of `ParsedListParams` the routes hand to
3
+ * the template — the `filters` field stays server-side. */
4
+ export interface ListViewParams {
5
+ search: string;
6
+ sort: SortSpec[];
7
+ page: number;
8
+ pageSize: number;
9
+ }
10
+ /** Rebuild the current list URL with `overrides` applied (a `null`
11
+ * override deletes that key). Mirrors the wire format parsed by
12
+ * `parseListParamsFromUrl`; returns `.` when no params remain so the
13
+ * link points at the bare route. */
14
+ export declare function buildHref(params: ListViewParams, overrides?: Record<string, string | null>): string;
15
+ /** Header link target that toggles the sort order for `field` (asc <->
16
+ * desc, defaulting to asc) and resets back to page 1. */
17
+ export declare function buildSortHref(params: ListViewParams, field: string): string;
18
+ /** Stringify a cell value for the list table: objects render as JSON,
19
+ * `null` / `undefined` render as the empty string. */
20
+ export declare function formatCell(value: unknown): string;
21
+ //# sourceMappingURL=list-href.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-href.d.ts","sourceRoot":"","sources":["../../src/list/list-href.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C;4DAC4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;qCAGqC;AACrC,wBAAgB,SAAS,CACvB,MAAM,EAAE,cAAc,EACtB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAM,GAC5C,MAAM,CAgBR;AAED;0DAC0D;AAC1D,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3E;AAED;uDACuD;AACvD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIjD"}
@@ -0,0 +1,50 @@
1
+ // Shared client-side helpers for the list / view route tables.
2
+ //
3
+ // `buildHref` / `buildSortHref` / `formatCell` were duplicated verbatim
4
+ // in the `/tables/[table]` and `/views/[view]` route components. This
5
+ // module is their single source so the shared `ListTable.svelte` and
6
+ // both routes agree on the URL contract defined in
7
+ // `query/list-params.ts`.
8
+ import { DEFAULT_PAGE_SIZE } from '../query/list-params.js';
9
+ /** Rebuild the current list URL with `overrides` applied (a `null`
10
+ * override deletes that key). Mirrors the wire format parsed by
11
+ * `parseListParamsFromUrl`; returns `.` when no params remain so the
12
+ * link points at the bare route. */
13
+ export function buildHref(params, overrides = {}) {
14
+ const sp = new URLSearchParams();
15
+ if (params.search.length > 0)
16
+ sp.set('q', params.search);
17
+ if (params.sort.length > 0) {
18
+ sp.set('sort', params.sort.map((s) => `${s.field}:${s.order}`).join(','));
19
+ }
20
+ if (params.page > 1)
21
+ sp.set('page', String(params.page));
22
+ if (params.pageSize !== DEFAULT_PAGE_SIZE) {
23
+ sp.set('pageSize', String(params.pageSize));
24
+ }
25
+ for (const [key, value] of Object.entries(overrides)) {
26
+ if (value === null)
27
+ sp.delete(key);
28
+ else
29
+ sp.set(key, value);
30
+ }
31
+ const query = sp.toString();
32
+ return query.length > 0 ? `?${query}` : '.';
33
+ }
34
+ /** Header link target that toggles the sort order for `field` (asc <->
35
+ * desc, defaulting to asc) and resets back to page 1. */
36
+ export function buildSortHref(params, field) {
37
+ const current = params.sort.find((s) => s.field === field)?.order;
38
+ const next = current === 'asc' ? 'desc' : 'asc';
39
+ return buildHref(params, { sort: `${field}:${next}`, page: null });
40
+ }
41
+ /** Stringify a cell value for the list table: objects render as JSON,
42
+ * `null` / `undefined` render as the empty string. */
43
+ export function formatCell(value) {
44
+ if (value === null || value === undefined)
45
+ return '';
46
+ if (typeof value === 'object')
47
+ return JSON.stringify(value);
48
+ return String(value);
49
+ }
50
+ //# sourceMappingURL=list-href.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-href.js","sourceRoot":"","sources":["../../src/list/list-href.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,EAAE;AACF,wEAAwE;AACxE,sEAAsE;AACtE,qEAAqE;AACrE,mDAAmD;AACnD,0BAA0B;AAI1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAW5D;;;qCAGqC;AACrC,MAAM,UAAU,SAAS,CACvB,MAAsB,EACtB,YAA2C,EAAE;IAE7C,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;QAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,QAAQ,KAAK,iBAAiB,EAAE,CAAC;QAC1C,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,IAAI,KAAK,KAAK,IAAI;YAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;;YAC9B,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC5B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC;AAED;0DAC0D;AAC1D,MAAM,UAAU,aAAa,CAAC,MAAsB,EAAE,KAAa;IACjE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,KAAK,CAAC;IAClE,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAChD,OAAO,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACrE,CAAC;AAED;uDACuD;AACvD,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ListParams, SortSpec } from '@kozou/core';
2
+ export declare const DEFAULT_PAGE_SIZE = 50;
3
+ export interface ParseListParamsInput {
4
+ url: URL;
5
+ /** Columns whose values can be ilike'd. Empty -> `q` is ignored. */
6
+ searchFields: string[];
7
+ defaultPageSize?: number;
8
+ }
9
+ export interface ParsedListParams extends ListParams {
10
+ /** The verbatim text from `?q=` so the UI can rehydrate the search box. */
11
+ search: string;
12
+ filters: Record<string, unknown>;
13
+ sort: SortSpec[];
14
+ page: number;
15
+ pageSize: number;
16
+ }
17
+ export declare function parseListParamsFromUrl(input: ParseListParamsInput): ParsedListParams;
18
+ //# sourceMappingURL=list-params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-params.d.ts","sourceRoot":"","sources":["../../src/query/list-params.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGxD,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,GAAG,CAAC;IACT,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,oBAAoB,GAC1B,gBAAgB,CAkBlB"}
@@ -0,0 +1,55 @@
1
+ // URL search-params <-> DataAdapter ListParams marshalling.
2
+ //
3
+ // Keeping the parser in a pure module lets the table route stay
4
+ // declarative and lets us unit-test the URL contract without
5
+ // booting SvelteKit. The wire format is:
6
+ //
7
+ // ?q=<text> -> case-insensitive search across `searchFields`
8
+ // ?sort=col:asc,col2:desc -> SortSpec[] (one or many)
9
+ // ?page=<n> -> 1-based page index, default 1
10
+ // ?pageSize=<m> -> rows per page, default 50
11
+ const DEFAULT_PAGE = 1;
12
+ export const DEFAULT_PAGE_SIZE = 50;
13
+ export function parseListParamsFromUrl(input) {
14
+ const { url, searchFields } = input;
15
+ const defaultPageSize = input.defaultPageSize ?? DEFAULT_PAGE_SIZE;
16
+ const params = url.searchParams;
17
+ const q = params.get('q') ?? '';
18
+ const sort = parseSortSpecs(params.get('sort'));
19
+ const page = parsePositiveInt(params.get('page'), DEFAULT_PAGE);
20
+ const pageSize = parsePositiveInt(params.get('pageSize'), defaultPageSize);
21
+ const filters = {};
22
+ if (q.length > 0 && searchFields.length > 0) {
23
+ filters.__or = searchFields
24
+ .map((field) => `${field}.ilike.*${q}*`)
25
+ .join(',');
26
+ }
27
+ return { search: q, filters, sort, page, pageSize };
28
+ }
29
+ function parseSortSpecs(raw) {
30
+ if (raw === null || raw.length === 0)
31
+ return [];
32
+ return raw
33
+ .split(',')
34
+ .map((entry) => parseSingleSort(entry))
35
+ .filter((entry) => entry !== null);
36
+ }
37
+ function parseSingleSort(raw) {
38
+ const trimmed = raw.trim();
39
+ if (trimmed.length === 0)
40
+ return null;
41
+ const [field, orderRaw] = trimmed.split(':');
42
+ if (!field)
43
+ return null;
44
+ const order = orderRaw === 'desc' ? 'desc' : 'asc';
45
+ return { field, order };
46
+ }
47
+ function parsePositiveInt(raw, fallback) {
48
+ if (raw === null)
49
+ return fallback;
50
+ const n = Number(raw);
51
+ if (!Number.isFinite(n) || n < 1)
52
+ return fallback;
53
+ return Math.floor(n);
54
+ }
55
+ //# sourceMappingURL=list-params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-params.js","sourceRoot":"","sources":["../../src/query/list-params.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,gEAAgE;AAChE,6DAA6D;AAC7D,yCAAyC;AACzC,EAAE;AACF,gFAAgF;AAChF,2DAA2D;AAC3D,gEAAgE;AAChE,4DAA4D;AAI5D,MAAM,YAAY,GAAG,CAAC,CAAC;AACvB,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAkBpC,MAAM,UAAU,sBAAsB,CACpC,KAA2B;IAE3B,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IACpC,MAAM,eAAe,GAAG,KAAK,CAAC,eAAe,IAAI,iBAAiB,CAAC;IACnE,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC;IAEhC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC,CAAC;IAE3E,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,IAAI,GAAG,YAAY;aACxB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,WAAW,CAAC,GAAG,CAAC;aACvC,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACtD,CAAC;AAED,SAAS,cAAc,CAAC,GAAkB;IACxC,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChD,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;SACtC,MAAM,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IACnD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAkB,EAAE,QAAgB;IAC5D,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IAClC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { ResourceId } from '@kozou/core';
2
+ /**
3
+ * Build the `[id]` path segment for a row from its primary-key columns.
4
+ * Returns `null` when the key is empty or any key column is missing/null on
5
+ * the row, so callers can fall back (e.g. to the row index for a keyed
6
+ * `{#each}`). A single-column key yields a plain encoded value; a composite
7
+ * key yields the comma-joined form.
8
+ */
9
+ export declare function rowIdSegment(row: Record<string, unknown>, primaryKey: string[]): string | null;
10
+ /**
11
+ * Encode a {@link ResourceId} back into the `[id]` path segment. The inverse
12
+ * of {@link parseResourceId}, used to build canonical detail / edit links
13
+ * and redirects from already-resolved key values.
14
+ */
15
+ export declare function encodeResourceId(id: ResourceId): string;
16
+ /**
17
+ * Parse the (SvelteKit-decoded) `[id]` route param into a {@link ResourceId}
18
+ * for the DataAdapter. A single-column key passes through verbatim (the value
19
+ * may contain a comma); a composite key splits on commas — the components are
20
+ * already URL-decoded by SvelteKit. The component count is not validated here;
21
+ * the adapter / server reports an arity mismatch.
22
+ */
23
+ export declare function parseResourceId(idParam: string, primaryKey: string[]): ResourceId;
24
+ //# sourceMappingURL=resource-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-id.d.ts","sourceRoot":"","sources":["../src/resource-id.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,UAAU,EAAE,MAAM,EAAE,GACnB,MAAM,GAAG,IAAI,CASf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,CAKvD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,GACnB,UAAU,CAKZ"}
@@ -0,0 +1,58 @@
1
+ // Composite-aware helpers for the `[id]` route segment.
2
+ //
3
+ // A single-column primary key is carried verbatim in the URL; a composite
4
+ // key joins its components — in `primaryKey` declaration order — into one
5
+ // path segment, each component percent-encoded and separated by an
6
+ // unescaped comma. The route stays a
7
+ // single dynamic `[id]` param; only the value shape changes, so the
8
+ // SvelteKit routing table is untouched.
9
+ //
10
+ // Limitation: a composite key value cannot itself contain a comma. The
11
+ // SvelteKit param (and the in-house API handler) URL-decode the whole
12
+ // segment before splitting on commas, so an encoded `%2C` is
13
+ // indistinguishable from a separator. Single-column keys are unaffected.
14
+ // This matches the server's documented limit.
15
+ /**
16
+ * Build the `[id]` path segment for a row from its primary-key columns.
17
+ * Returns `null` when the key is empty or any key column is missing/null on
18
+ * the row, so callers can fall back (e.g. to the row index for a keyed
19
+ * `{#each}`). A single-column key yields a plain encoded value; a composite
20
+ * key yields the comma-joined form.
21
+ */
22
+ export function rowIdSegment(row, primaryKey) {
23
+ if (primaryKey.length === 0)
24
+ return null;
25
+ const parts = [];
26
+ for (const column of primaryKey) {
27
+ const value = row[column];
28
+ if (value === undefined || value === null)
29
+ return null;
30
+ parts.push(encodeURIComponent(String(value)));
31
+ }
32
+ return parts.join(',');
33
+ }
34
+ /**
35
+ * Encode a {@link ResourceId} back into the `[id]` path segment. The inverse
36
+ * of {@link parseResourceId}, used to build canonical detail / edit links
37
+ * and redirects from already-resolved key values.
38
+ */
39
+ export function encodeResourceId(id) {
40
+ if (Array.isArray(id)) {
41
+ return id.map((part) => encodeURIComponent(String(part))).join(',');
42
+ }
43
+ return encodeURIComponent(String(id));
44
+ }
45
+ /**
46
+ * Parse the (SvelteKit-decoded) `[id]` route param into a {@link ResourceId}
47
+ * for the DataAdapter. A single-column key passes through verbatim (the value
48
+ * may contain a comma); a composite key splits on commas — the components are
49
+ * already URL-decoded by SvelteKit. The component count is not validated here;
50
+ * the adapter / server reports an arity mismatch.
51
+ */
52
+ export function parseResourceId(idParam, primaryKey) {
53
+ if (primaryKey.length > 1) {
54
+ return idParam.split(',');
55
+ }
56
+ return idParam;
57
+ }
58
+ //# sourceMappingURL=resource-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-id.js","sourceRoot":"","sources":["../src/resource-id.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,mEAAmE;AACnE,qCAAqC;AACrC,oEAAoE;AACpE,wCAAwC;AACxC,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,6DAA6D;AAC7D,yEAAyE;AACzE,8CAA8C;AAI9C;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,GAA4B,EAC5B,UAAoB;IAEpB,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACvD,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAc;IAC7C,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,UAAoB;IAEpB,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { ResourceId } from '@kozou/core';
2
+ export interface FkRowCacheOptions {
3
+ /** Cache lifetime per entry, in milliseconds. Defaults to 60_000
4
+ * to match the SchemaCache TTL so a single render cycle re-uses
5
+ * hot rows. */
6
+ ttlMs?: number;
7
+ /** Injectable clock for unit tests. */
8
+ now?: () => number;
9
+ }
10
+ export type FkRowLoader = (qualifiedName: string, id: ResourceId) => Promise<Record<string, unknown> | null>;
11
+ export declare class FkRowCache {
12
+ private readonly ttlMs;
13
+ private readonly now;
14
+ private readonly entries;
15
+ constructor(opts?: FkRowCacheOptions);
16
+ /** Return a cached row when one is fresh; otherwise run `loader` and
17
+ * cache its result (including `null`). The loader is responsible
18
+ * for swallowing network / adapter errors and resolving to `null`
19
+ * so callers see a uniform value-or-null contract. */
20
+ get(qualifiedName: string, id: ResourceId, loader: FkRowLoader): Promise<Record<string, unknown> | null>;
21
+ /** Drop every cached entry. Mainly used by tests; production code
22
+ * leans on TTL expiry. */
23
+ clear(): void;
24
+ /** Visible for tests. */
25
+ size(): number;
26
+ }
27
+ //# sourceMappingURL=fk-row-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fk-row-cache.d.ts","sourceRoot":"","sources":["../../src/server/fk-row-cache.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,MAAM,WAAW,iBAAiB;IAChC;;oBAEgB;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,CACxB,aAAa,EAAE,MAAM,EACrB,EAAE,EAAE,UAAU,KACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;AAO7C,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;gBAE7C,IAAI,GAAE,iBAAsB;IAKxC;;;2DAGuD;IACjD,GAAG,CACP,aAAa,EAAE,MAAM,EACrB,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAY1C;+BAC2B;IAC3B,KAAK,IAAI,IAAI;IAIb,yBAAyB;IACzB,IAAI,IAAI,MAAM;CAGf"}
@@ -0,0 +1,54 @@
1
+ // Server-side TTL cache for foreign key target rows.
2
+ //
3
+ // The detail route resolves each FK column on the page to the
4
+ // referenced row's displayField label. A naive lookup would hit the
5
+ // DataAdapter once per FK column per render; this cache keeps the
6
+ // resolved rows around for `ttlMs` so navigating between sibling
7
+ // detail pages (or re-rendering the same row) does not re-fetch the
8
+ // same target. Tracks FK label resolution via hooks.server TTL cache.
9
+ //
10
+ // The cache stores null on misses too. A real fetch error (network
11
+ // blip, 5xx) is caught by the caller's loader and surfaced as null,
12
+ // so subsequent renders within the TTL window keep showing the raw
13
+ // value rather than re-issuing the failing request; the TTL is short
14
+ // enough (default 60s) that transient failures self-heal.
15
+ import { encodeResourceId } from '../resource-id.js';
16
+ export class FkRowCache {
17
+ ttlMs;
18
+ now;
19
+ entries = new Map();
20
+ constructor(opts = {}) {
21
+ this.ttlMs = opts.ttlMs ?? 60_000;
22
+ this.now = opts.now ?? Date.now;
23
+ }
24
+ /** Return a cached row when one is fresh; otherwise run `loader` and
25
+ * cache its result (including `null`). The loader is responsible
26
+ * for swallowing network / adapter errors and resolving to `null`
27
+ * so callers see a uniform value-or-null contract. */
28
+ async get(qualifiedName, id, loader) {
29
+ const key = makeKey(qualifiedName, id);
30
+ const existing = this.entries.get(key);
31
+ const now = this.now();
32
+ if (existing !== undefined && now - existing.fetchedAt < this.ttlMs) {
33
+ return existing.row;
34
+ }
35
+ const row = await loader(qualifiedName, id);
36
+ this.entries.set(key, { row, fetchedAt: now });
37
+ return row;
38
+ }
39
+ /** Drop every cached entry. Mainly used by tests; production code
40
+ * leans on TTL expiry. */
41
+ clear() {
42
+ this.entries.clear();
43
+ }
44
+ /** Visible for tests. */
45
+ size() {
46
+ return this.entries.size;
47
+ }
48
+ }
49
+ // The canonical encoded id keeps a composite key (raw-comma joined) distinct
50
+ // from a scalar that happens to contain a comma (percent-encoded).
51
+ function makeKey(qualifiedName, id) {
52
+ return `${qualifiedName}:${encodeResourceId(id)}`;
53
+ }
54
+ //# sourceMappingURL=fk-row-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fk-row-cache.js","sourceRoot":"","sources":["../../src/server/fk-row-cache.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,EAAE;AACF,8DAA8D;AAC9D,oEAAoE;AACpE,kEAAkE;AAClE,iEAAiE;AACjE,oEAAoE;AACpE,sEAAsE;AACtE,EAAE;AACF,mEAAmE;AACnE,oEAAoE;AACpE,mEAAmE;AACnE,qEAAqE;AACrE,0DAA0D;AAI1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAqBrD,MAAM,OAAO,UAAU;IACJ,KAAK,CAAS;IACd,GAAG,CAAe;IAClB,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEzD,YAAY,OAA0B,EAAE;QACtC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;QAClC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IAClC,CAAC;IAED;;;2DAGuD;IACvD,KAAK,CAAC,GAAG,CACP,aAAqB,EACrB,EAAc,EACd,MAAmB;QAEnB,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,QAAQ,KAAK,SAAS,IAAI,GAAG,GAAG,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACpE,OAAO,QAAQ,CAAC,GAAG,CAAC;QACtB,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAC/C,OAAO,GAAG,CAAC;IACb,CAAC;IAED;+BAC2B;IAC3B,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,yBAAyB;IACzB,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED,6EAA6E;AAC7E,mEAAmE;AACnE,SAAS,OAAO,CAAC,aAAqB,EAAE,EAAc;IACpD,OAAO,GAAG,aAAa,IAAI,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;AACpD,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { SchemaContext } from '@kozou/core';
2
+ export type SchemaLoader = () => Promise<SchemaContext>;
3
+ export type Clock = () => number;
4
+ export interface SchemaCacheOptions {
5
+ loader: SchemaLoader;
6
+ /** Cache TTL in milliseconds. Defaults to 60_000. */
7
+ ttlMs?: number;
8
+ /** Time source. Defaults to Date.now. */
9
+ clock?: Clock;
10
+ }
11
+ export declare class SchemaCache {
12
+ private value;
13
+ private lastBuiltAt;
14
+ private inflight;
15
+ private readonly loader;
16
+ private readonly ttlMs;
17
+ private readonly clock;
18
+ constructor(opts: SchemaCacheOptions);
19
+ get(): Promise<SchemaContext>;
20
+ invalidate(): void;
21
+ }
22
+ //# sourceMappingURL=schema-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cache.d.ts","sourceRoot":"","sources":["../../src/server/schema-cache.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AACxD,MAAM,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC;AAEjC,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAC;IACrB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;gBAElB,IAAI,EAAE,kBAAkB;IAM9B,GAAG,IAAI,OAAO,CAAC,aAAa,CAAC;IAqBnC,UAAU,IAAI,IAAI;CAInB"}
@@ -0,0 +1,46 @@
1
+ // In-process SchemaContext cache.
2
+ // Wraps a loader behind a TTL and a single in-flight promise so that
3
+ // concurrent SvelteKit requests cannot trigger duplicate introspect
4
+ // calls against PostgreSQL. The hooks.server.ts module composes this
5
+ // with the @kozou/introspect + @kozou/core pipeline; tests inject a
6
+ // stub loader + clock to keep the module pure.
7
+ const DEFAULT_TTL_MS = 60_000;
8
+ export class SchemaCache {
9
+ value = null;
10
+ lastBuiltAt = 0;
11
+ inflight = null;
12
+ loader;
13
+ ttlMs;
14
+ clock;
15
+ constructor(opts) {
16
+ this.loader = opts.loader;
17
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
18
+ this.clock = opts.clock ?? Date.now;
19
+ }
20
+ async get() {
21
+ const now = this.clock();
22
+ if (this.value !== null && now - this.lastBuiltAt <= this.ttlMs) {
23
+ return this.value;
24
+ }
25
+ if (this.inflight !== null) {
26
+ return this.inflight;
27
+ }
28
+ this.inflight = (async () => {
29
+ try {
30
+ const next = await this.loader();
31
+ this.value = next;
32
+ this.lastBuiltAt = this.clock();
33
+ return next;
34
+ }
35
+ finally {
36
+ this.inflight = null;
37
+ }
38
+ })();
39
+ return this.inflight;
40
+ }
41
+ invalidate() {
42
+ this.value = null;
43
+ this.lastBuiltAt = 0;
44
+ }
45
+ }
46
+ //# sourceMappingURL=schema-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cache.js","sourceRoot":"","sources":["../../src/server/schema-cache.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,qEAAqE;AACrE,oEAAoE;AACpE,qEAAqE;AACrE,oEAAoE;AACpE,+CAA+C;AAe/C,MAAM,cAAc,GAAG,MAAM,CAAC;AAE9B,MAAM,OAAO,WAAW;IACd,KAAK,GAAyB,IAAI,CAAC;IACnC,WAAW,GAAG,CAAC,CAAC;IAChB,QAAQ,GAAkC,IAAI,CAAC;IACtC,MAAM,CAAe;IACrB,KAAK,CAAS;IACd,KAAK,CAAQ;IAE9B,YAAY,IAAwB;QAClC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,GAAG;QACP,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAChE,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;gBAClB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;gBAChC,OAAO,IAAI,CAAC;YACd,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ import type { ColumnContext, ViewContext } from '@kozou/core';
2
+ export declare function pickViewDisplayColumns(view: ViewContext): ColumnContext[];
3
+ export declare function pickViewSearchFields(view: ViewContext): string[];
4
+ //# sourceMappingURL=columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../src/view/columns.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa9D,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,EAAE,CAEzE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAQhE"}
@@ -0,0 +1,22 @@
1
+ // View display + search field pickers.
2
+ // Symmetrical to the equivalents inside the table list route, but
3
+ // kept in a separate module so the view route can stay declarative
4
+ // and the heuristics are unit-testable in isolation.
5
+ const DISPLAY_COLUMN_LIMIT = 5;
6
+ const TEXT_LIKE_TYPE_PREFIXES = [
7
+ 'text',
8
+ 'character',
9
+ 'varchar',
10
+ 'citext',
11
+ 'name',
12
+ 'uuid',
13
+ ];
14
+ export function pickViewDisplayColumns(view) {
15
+ return view.columns.slice(0, DISPLAY_COLUMN_LIMIT);
16
+ }
17
+ export function pickViewSearchFields(view) {
18
+ return view.columns
19
+ .filter((c) => TEXT_LIKE_TYPE_PREFIXES.some((prefix) => c.dataType.toLowerCase().startsWith(prefix)))
20
+ .map((c) => c.name);
21
+ }
22
+ //# sourceMappingURL=columns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.js","sourceRoot":"","sources":["../../src/view/columns.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kEAAkE;AAClE,mEAAmE;AACnE,qDAAqD;AAIrD,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B,MAAM,uBAAuB,GAAG;IAC9B,MAAM;IACN,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,MAAM;CACP,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,IAAiB;IACtD,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAiB;IACpD,OAAO,IAAI,CAAC,OAAO;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACZ,uBAAuB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACtC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAC5C,CACF;SACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@kozou/ui-core",
3
+ "version": "1.7.0",
4
+ "description": "Kozou framework-agnostic UI core: the read-path logic (DataAdapter implementations, list params, view columns, cell formatting, FK label resolution, resource ids, schema/FK caches) that turns a SchemaContext + DataAdapter into the data a reference Admin UI renders.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kozou-dev/kozou.git",
9
+ "directory": "packages/ui-core"
10
+ },
11
+ "homepage": "https://kozou.org",
12
+ "bugs": {
13
+ "url": "https://github.com/kozou-dev/kozou/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "provenance": true
32
+ },
33
+ "dependencies": {
34
+ "@kozou/core": "1.7.0"
35
+ },
36
+ "scripts": {
37
+ "typecheck": "tsc -p tsconfig.test.json",
38
+ "build": "tsc",
39
+ "test": "vitest run --coverage"
40
+ }
41
+ }