@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.
- package/LICENSE +202 -0
- package/README.md +40 -0
- package/dist/adapter/errors.d.ts +16 -0
- package/dist/adapter/errors.d.ts.map +1 -0
- package/dist/adapter/errors.js +19 -0
- package/dist/adapter/errors.js.map +1 -0
- package/dist/adapter/index.d.ts +8 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +15 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter/kozou-api.d.ts +35 -0
- package/dist/adapter/kozou-api.d.ts.map +1 -0
- package/dist/adapter/kozou-api.js +215 -0
- package/dist/adapter/kozou-api.js.map +1 -0
- package/dist/adapter/postgrest.d.ts +51 -0
- package/dist/adapter/postgrest.d.ts.map +1 -0
- package/dist/adapter/postgrest.js +343 -0
- package/dist/adapter/postgrest.js.map +1 -0
- package/dist/adapter/types.d.ts +6 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/adapter/types.js +3 -0
- package/dist/adapter/types.js.map +1 -0
- package/dist/detail/format-cell.d.ts +7 -0
- package/dist/detail/format-cell.d.ts.map +1 -0
- package/dist/detail/format-cell.js +50 -0
- package/dist/detail/format-cell.js.map +1 -0
- package/dist/detail/resolve-fk-labels.d.ts +23 -0
- package/dist/detail/resolve-fk-labels.d.ts.map +1 -0
- package/dist/detail/resolve-fk-labels.js +118 -0
- package/dist/detail/resolve-fk-labels.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/list/list-href.d.ts +21 -0
- package/dist/list/list-href.d.ts.map +1 -0
- package/dist/list/list-href.js +50 -0
- package/dist/list/list-href.js.map +1 -0
- package/dist/query/list-params.d.ts +18 -0
- package/dist/query/list-params.d.ts.map +1 -0
- package/dist/query/list-params.js +55 -0
- package/dist/query/list-params.js.map +1 -0
- package/dist/resource-id.d.ts +24 -0
- package/dist/resource-id.d.ts.map +1 -0
- package/dist/resource-id.js +58 -0
- package/dist/resource-id.js.map +1 -0
- package/dist/server/fk-row-cache.d.ts +27 -0
- package/dist/server/fk-row-cache.d.ts.map +1 -0
- package/dist/server/fk-row-cache.js +54 -0
- package/dist/server/fk-row-cache.js.map +1 -0
- package/dist/server/schema-cache.d.ts +22 -0
- package/dist/server/schema-cache.d.ts.map +1 -0
- package/dist/server/schema-cache.js +46 -0
- package/dist/server/schema-cache.js.map +1 -0
- package/dist/view/columns.d.ts +4 -0
- package/dist/view/columns.d.ts.map +1 -0
- package/dist/view/columns.js +22 -0
- package/dist/view/columns.js.map +1 -0
- 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 @@
|
|
|
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
|
+
}
|