@loj-lang/rdsl-runtime 0.5.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/README.md +19 -0
- package/dist/components/Badge.d.ts +6 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +5 -0
- package/dist/components/ConfirmDialog.d.ts +8 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +11 -0
- package/dist/components/DataTable.d.ts +34 -0
- package/dist/components/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable.js +58 -0
- package/dist/components/FilterBar.d.ts +13 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +41 -0
- package/dist/components/FormField.d.ts +22 -0
- package/dist/components/FormField.d.ts.map +1 -0
- package/dist/components/FormField.js +66 -0
- package/dist/components/GroupedDataTable.d.ts +26 -0
- package/dist/components/GroupedDataTable.d.ts.map +1 -0
- package/dist/components/GroupedDataTable.js +67 -0
- package/dist/components/Pagination.d.ts +7 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +12 -0
- package/dist/components/PivotDataTable.d.ts +25 -0
- package/dist/components/PivotDataTable.d.ts.map +1 -0
- package/dist/components/PivotDataTable.js +72 -0
- package/dist/components/Tag.d.ts +6 -0
- package/dist/components/Tag.d.ts.map +1 -0
- package/dist/components/Tag.js +5 -0
- package/dist/components/WorkflowSummary.d.ts +5 -0
- package/dist/components/WorkflowSummary.d.ts.map +1 -0
- package/dist/components/WorkflowSummary.js +17 -0
- package/dist/hooks/navigation.d.ts +20 -0
- package/dist/hooks/navigation.d.ts.map +1 -0
- package/dist/hooks/navigation.js +135 -0
- package/dist/hooks/resourceClient.d.ts +36 -0
- package/dist/hooks/resourceClient.d.ts.map +1 -0
- package/dist/hooks/resourceClient.js +259 -0
- package/dist/hooks/resourceStore.d.ts +25 -0
- package/dist/hooks/resourceStore.d.ts.map +1 -0
- package/dist/hooks/resourceStore.js +164 -0
- package/dist/hooks/resourceTarget.d.ts +2 -0
- package/dist/hooks/resourceTarget.d.ts.map +1 -0
- package/dist/hooks/resourceTarget.js +22 -0
- package/dist/hooks/useAuth.d.ts +16 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +20 -0
- package/dist/hooks/useCollectionView.d.ts +28 -0
- package/dist/hooks/useCollectionView.d.ts.map +1 -0
- package/dist/hooks/useCollectionView.js +73 -0
- package/dist/hooks/useDocumentMetadata.d.ts +13 -0
- package/dist/hooks/useDocumentMetadata.d.ts.map +1 -0
- package/dist/hooks/useDocumentMetadata.js +111 -0
- package/dist/hooks/useGroupedCollectionView.d.ts +26 -0
- package/dist/hooks/useGroupedCollectionView.d.ts.map +1 -0
- package/dist/hooks/useGroupedCollectionView.js +82 -0
- package/dist/hooks/useReadModel.d.ts +16 -0
- package/dist/hooks/useReadModel.d.ts.map +1 -0
- package/dist/hooks/useReadModel.js +104 -0
- package/dist/hooks/useResource.d.ts +36 -0
- package/dist/hooks/useResource.d.ts.map +1 -0
- package/dist/hooks/useResource.js +69 -0
- package/dist/hooks/useToast.d.ts +18 -0
- package/dist/hooks/useToast.d.ts.map +1 -0
- package/dist/hooks/useToast.js +31 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/policies/can.d.ts +15 -0
- package/dist/policies/can.d.ts.map +1 -0
- package/dist/policies/can.js +160 -0
- package/package.json +55 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
let configuredAppBasePath = '/';
|
|
2
|
+
export function normalizeAppBasePath(value) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return '/';
|
|
5
|
+
}
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed || trimmed === '/') {
|
|
8
|
+
return '/';
|
|
9
|
+
}
|
|
10
|
+
const prefixed = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
11
|
+
return prefixed.endsWith('/') ? prefixed.slice(0, -1) || '/' : prefixed;
|
|
12
|
+
}
|
|
13
|
+
export function configureAppBasePath(value) {
|
|
14
|
+
configuredAppBasePath = normalizeAppBasePath(value);
|
|
15
|
+
}
|
|
16
|
+
export function getConfiguredAppBasePath() {
|
|
17
|
+
return configuredAppBasePath;
|
|
18
|
+
}
|
|
19
|
+
export function prefixAppBasePath(path) {
|
|
20
|
+
if (!path.startsWith('/')) {
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
const basePath = getConfiguredAppBasePath();
|
|
24
|
+
if (basePath === '/') {
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
if (path === basePath || path.startsWith(`${basePath}/`)) {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
if (path === '/') {
|
|
31
|
+
return basePath;
|
|
32
|
+
}
|
|
33
|
+
return `${basePath}${path}`;
|
|
34
|
+
}
|
|
35
|
+
export function stripAppBasePath(pathname) {
|
|
36
|
+
const normalizedPathname = pathname && pathname.startsWith('/') ? pathname : `/${pathname || ''}`;
|
|
37
|
+
const basePath = getConfiguredAppBasePath();
|
|
38
|
+
if (basePath === '/') {
|
|
39
|
+
return normalizedPathname === '' ? '/' : normalizedPathname;
|
|
40
|
+
}
|
|
41
|
+
if (normalizedPathname === basePath) {
|
|
42
|
+
return '/';
|
|
43
|
+
}
|
|
44
|
+
if (normalizedPathname.startsWith(`${basePath}/`)) {
|
|
45
|
+
return normalizedPathname.slice(basePath.length) || '/';
|
|
46
|
+
}
|
|
47
|
+
return normalizedPathname === '' ? '/' : normalizedPathname;
|
|
48
|
+
}
|
|
49
|
+
export function getCurrentAppPathname() {
|
|
50
|
+
if (typeof window === 'undefined')
|
|
51
|
+
return '/';
|
|
52
|
+
return stripAppBasePath(window.location.pathname);
|
|
53
|
+
}
|
|
54
|
+
export function getCurrentAppHref() {
|
|
55
|
+
if (typeof window === 'undefined')
|
|
56
|
+
return '/';
|
|
57
|
+
return `${window.location.pathname}${window.location.search}`;
|
|
58
|
+
}
|
|
59
|
+
export function getLocationSearchParams() {
|
|
60
|
+
if (typeof window === 'undefined')
|
|
61
|
+
return null;
|
|
62
|
+
return new URLSearchParams(window.location.search);
|
|
63
|
+
}
|
|
64
|
+
export function shiftDateInputValue(value, days) {
|
|
65
|
+
const trimmed = String(value ?? '').trim();
|
|
66
|
+
const match = trimmed.match(/^(\d{4})([-/])(\d{2})\2(\d{2})$/);
|
|
67
|
+
if (!match) {
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
const [, yearText, separator, monthText, dayText] = match;
|
|
71
|
+
const year = Number(yearText);
|
|
72
|
+
const month = Number(monthText);
|
|
73
|
+
const day = Number(dayText);
|
|
74
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|
|
77
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
78
|
+
if (Number.isNaN(date.getTime())
|
|
79
|
+
|| date.getUTCFullYear() !== year
|
|
80
|
+
|| date.getUTCMonth() !== month - 1
|
|
81
|
+
|| date.getUTCDate() !== day) {
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
85
|
+
const nextYear = String(date.getUTCFullYear()).padStart(4, '0');
|
|
86
|
+
const nextMonth = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
87
|
+
const nextDay = String(date.getUTCDate()).padStart(2, '0');
|
|
88
|
+
return `${nextYear}${separator}${nextMonth}${separator}${nextDay}`;
|
|
89
|
+
}
|
|
90
|
+
function scopedSearchParamKey(prefix, key) {
|
|
91
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
92
|
+
}
|
|
93
|
+
export function getLocationSearchValues(defaultValues, options = {}) {
|
|
94
|
+
const params = options.searchParams ?? getLocationSearchParams();
|
|
95
|
+
const nextValues = { ...defaultValues };
|
|
96
|
+
for (const key of Object.keys(defaultValues)) {
|
|
97
|
+
const value = params?.get(scopedSearchParamKey(options.prefix, key));
|
|
98
|
+
if (value !== null && value !== undefined) {
|
|
99
|
+
nextValues[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return nextValues;
|
|
103
|
+
}
|
|
104
|
+
export function replaceLocationSearchValues(values, options = {}) {
|
|
105
|
+
if (typeof window === 'undefined') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const keys = options.keys ?? Object.keys(values);
|
|
109
|
+
const params = new URLSearchParams(window.location.search);
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const value = String(values[key] ?? '');
|
|
112
|
+
const scopedKey = scopedSearchParamKey(options.prefix, key);
|
|
113
|
+
if (value.trim() === '') {
|
|
114
|
+
params.delete(scopedKey);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
params.set(scopedKey, value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const nextSearch = params.toString();
|
|
121
|
+
const nextHref = `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ''}${window.location.hash}`;
|
|
122
|
+
const currentHref = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
123
|
+
if (nextHref !== currentHref) {
|
|
124
|
+
window.history.replaceState(window.history.state, '', nextHref);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function sanitizeAppLocalHref(candidate) {
|
|
128
|
+
if (!candidate || !candidate.startsWith('/') || candidate.startsWith('//'))
|
|
129
|
+
return null;
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
export function getSanitizedReturnTo(searchParams) {
|
|
133
|
+
const params = searchParams ?? getLocationSearchParams();
|
|
134
|
+
return sanitizeAppLocalHref(params?.get('returnTo'));
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ResourceClient {
|
|
2
|
+
get<T>(path: string): Promise<T>;
|
|
3
|
+
list<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>(api: string): Promise<T[]>;
|
|
6
|
+
create<T extends {
|
|
7
|
+
id: string;
|
|
8
|
+
}>(api: string, input: Partial<T>): Promise<T>;
|
|
9
|
+
update<T extends {
|
|
10
|
+
id: string;
|
|
11
|
+
}>(api: string, id: string, input: Partial<T>): Promise<T>;
|
|
12
|
+
delete(api: string, id: string): Promise<void>;
|
|
13
|
+
post<T>(path: string, input?: unknown): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
export interface ResourceProviderProps {
|
|
16
|
+
client: ResourceClient;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export interface FetchResourceClientOptions {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
22
|
+
credentials?: RequestCredentials;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
export declare function normalizeListPayload<T extends {
|
|
26
|
+
id: string;
|
|
27
|
+
}>(payload: unknown, api: string): T[];
|
|
28
|
+
export declare function normalizeRecordPayload<T extends {
|
|
29
|
+
id: string;
|
|
30
|
+
}>(payload: unknown, api: string, operation: string): T;
|
|
31
|
+
export declare function createFetchResourceClient(options?: FetchResourceClientOptions): ResourceClient;
|
|
32
|
+
export declare function createMemoryResourceClient(): ResourceClient;
|
|
33
|
+
export declare function ResourceProvider({ client, children }: ResourceProviderProps): any;
|
|
34
|
+
export declare function useResourceClient(): ResourceClient;
|
|
35
|
+
export declare function resetResourceClientTestState(): void;
|
|
36
|
+
//# sourceMappingURL=resourceClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resourceClient.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceClient.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1D,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,CAAC,CAAC,SAAS;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzF,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AA2DD,wBAAgB,oBAAoB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAC3D,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,GACV,CAAC,EAAE,CAcL;AAED,wBAAgB,sBAAsB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAC7D,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,CAAC,CAQH;AA4CD,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,cAAc,CAkFlG;AAED,wBAAgB,0BAA0B,IAAI,cAAc,CAwC3D;AAqCD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,qBAAqB,OAE3E;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAKnD"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const ResourceClientContext = React.createContext(undefined);
|
|
3
|
+
const memoryCollections = new Map();
|
|
4
|
+
const fetchClientCache = new Map();
|
|
5
|
+
let sameOriginFetchClient = null;
|
|
6
|
+
let defaultMemoryClient = null;
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function resourceLeaf(api) {
|
|
11
|
+
const parts = api.split('/').filter(Boolean);
|
|
12
|
+
return parts[parts.length - 1] ?? 'record';
|
|
13
|
+
}
|
|
14
|
+
function createSyntheticId(api, collection) {
|
|
15
|
+
const id = `${resourceLeaf(api)}-${collection.nextId}`;
|
|
16
|
+
collection.nextId += 1;
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
function ensureMemoryCollection(api) {
|
|
20
|
+
const existing = memoryCollections.get(api);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const created = {
|
|
24
|
+
items: [],
|
|
25
|
+
nextId: 1,
|
|
26
|
+
};
|
|
27
|
+
memoryCollections.set(api, created);
|
|
28
|
+
return created;
|
|
29
|
+
}
|
|
30
|
+
function withStringId(payload, api, operation) {
|
|
31
|
+
if (!isRecord(payload) || payload.id === undefined || payload.id === null) {
|
|
32
|
+
throw new Error(`Invalid ${operation} response for ${api}: expected a record with an id`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...payload,
|
|
36
|
+
id: String(payload.id),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function normalizeListPayload(payload, api) {
|
|
40
|
+
const items = Array.isArray(payload)
|
|
41
|
+
? payload
|
|
42
|
+
: isRecord(payload) && Array.isArray(payload.items)
|
|
43
|
+
? payload.items
|
|
44
|
+
: isRecord(payload) && Array.isArray(payload.data)
|
|
45
|
+
? payload.data
|
|
46
|
+
: null;
|
|
47
|
+
if (!items) {
|
|
48
|
+
throw new Error(`Invalid list response for ${api}: expected an array or { items: [] }`);
|
|
49
|
+
}
|
|
50
|
+
return items.map((item) => withStringId(item, api, 'list'));
|
|
51
|
+
}
|
|
52
|
+
export function normalizeRecordPayload(payload, api, operation) {
|
|
53
|
+
const record = isRecord(payload) && isRecord(payload.item)
|
|
54
|
+
? payload.item
|
|
55
|
+
: isRecord(payload) && isRecord(payload.data)
|
|
56
|
+
? payload.data
|
|
57
|
+
: payload;
|
|
58
|
+
return withStringId(record, api, operation);
|
|
59
|
+
}
|
|
60
|
+
async function resolveHeaders(headers) {
|
|
61
|
+
if (!headers)
|
|
62
|
+
return undefined;
|
|
63
|
+
return typeof headers === 'function' ? await headers() : headers;
|
|
64
|
+
}
|
|
65
|
+
function mergeHeaders(...headerSets) {
|
|
66
|
+
const merged = new Headers();
|
|
67
|
+
for (const headerSet of headerSets) {
|
|
68
|
+
if (!headerSet)
|
|
69
|
+
continue;
|
|
70
|
+
const next = new Headers(headerSet);
|
|
71
|
+
next.forEach((value, key) => {
|
|
72
|
+
merged.set(key, value);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
function resolveResourceUrl(api, baseUrl) {
|
|
78
|
+
if (/^https?:\/\//i.test(api))
|
|
79
|
+
return api;
|
|
80
|
+
if (!baseUrl || baseUrl.trim() === '')
|
|
81
|
+
return api;
|
|
82
|
+
const normalizedBase = baseUrl.trim();
|
|
83
|
+
if (!/^https?:\/\//i.test(normalizedBase)) {
|
|
84
|
+
return api;
|
|
85
|
+
}
|
|
86
|
+
const absoluteBase = normalizedBase.endsWith('/') ? normalizedBase : `${normalizedBase}/`;
|
|
87
|
+
return new URL(api.startsWith('/') ? api.slice(1) : api, absoluteBase).toString();
|
|
88
|
+
}
|
|
89
|
+
async function readJsonPayload(response, api, method) {
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
if (text.trim() === '')
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`Invalid JSON response for ${method} ${api}: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function createFetchResourceClient(options = {}) {
|
|
101
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
102
|
+
if (typeof fetchImpl !== 'function') {
|
|
103
|
+
throw new Error('Fetch is not available; provide options.fetch or use ResourceProvider with a custom client');
|
|
104
|
+
}
|
|
105
|
+
async function request(method, api, init = {}) {
|
|
106
|
+
const requestHeaders = mergeHeaders(await resolveHeaders(options.headers), init.headers);
|
|
107
|
+
const response = await fetchImpl(resolveResourceUrl(api, options.baseUrl), {
|
|
108
|
+
...init,
|
|
109
|
+
method,
|
|
110
|
+
credentials: options.credentials,
|
|
111
|
+
headers: requestHeaders,
|
|
112
|
+
});
|
|
113
|
+
const payload = await readJsonPayload(response, api, method);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const detail = isRecord(payload) && typeof payload.message === 'string'
|
|
116
|
+
? `: ${payload.message}`
|
|
117
|
+
: '';
|
|
118
|
+
throw new Error(`${method} ${api} failed with ${response.status}${detail}`);
|
|
119
|
+
}
|
|
120
|
+
return payload;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
async get(path) {
|
|
124
|
+
const payload = await request('GET', path);
|
|
125
|
+
return payload;
|
|
126
|
+
},
|
|
127
|
+
async list(api) {
|
|
128
|
+
const payload = await request('GET', api);
|
|
129
|
+
return normalizeListPayload(payload, api);
|
|
130
|
+
},
|
|
131
|
+
async create(api, input) {
|
|
132
|
+
const payload = await request('POST', api, {
|
|
133
|
+
body: JSON.stringify(input),
|
|
134
|
+
headers: {
|
|
135
|
+
Accept: 'application/json',
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return normalizeRecordPayload(payload, api, 'create');
|
|
140
|
+
},
|
|
141
|
+
async update(api, id, input) {
|
|
142
|
+
const payload = await request('PUT', `${api}/${encodeURIComponent(id)}`, {
|
|
143
|
+
body: JSON.stringify(input),
|
|
144
|
+
headers: {
|
|
145
|
+
Accept: 'application/json',
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
if (payload === undefined) {
|
|
150
|
+
return {
|
|
151
|
+
...input,
|
|
152
|
+
id: String(id),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return normalizeRecordPayload(payload, api, 'update');
|
|
156
|
+
},
|
|
157
|
+
async delete(api, id) {
|
|
158
|
+
await request('DELETE', `${api}/${encodeURIComponent(id)}`);
|
|
159
|
+
},
|
|
160
|
+
async post(path, input) {
|
|
161
|
+
const payload = await request('POST', path, {
|
|
162
|
+
body: input === undefined ? undefined : JSON.stringify(input),
|
|
163
|
+
headers: {
|
|
164
|
+
Accept: 'application/json',
|
|
165
|
+
...(input === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return payload;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function createMemoryResourceClient() {
|
|
173
|
+
return {
|
|
174
|
+
async get(path) {
|
|
175
|
+
const [collectionPath] = path.split('?');
|
|
176
|
+
return ensureMemoryCollection(collectionPath).items.map((item) => ({ ...item }));
|
|
177
|
+
},
|
|
178
|
+
async list(api) {
|
|
179
|
+
return ensureMemoryCollection(api).items.map((item) => ({ ...item }));
|
|
180
|
+
},
|
|
181
|
+
async create(api, input) {
|
|
182
|
+
const collection = ensureMemoryCollection(api);
|
|
183
|
+
const nextRecord = {
|
|
184
|
+
...input,
|
|
185
|
+
id: String(input.id ?? createSyntheticId(api, collection)),
|
|
186
|
+
};
|
|
187
|
+
collection.items = [...collection.items, nextRecord];
|
|
188
|
+
return { ...nextRecord };
|
|
189
|
+
},
|
|
190
|
+
async update(api, id, input) {
|
|
191
|
+
const collection = ensureMemoryCollection(api);
|
|
192
|
+
const current = collection.items.find((item) => item.id === id);
|
|
193
|
+
if (!current) {
|
|
194
|
+
throw new Error(`Record not found: ${id}`);
|
|
195
|
+
}
|
|
196
|
+
const nextRecord = {
|
|
197
|
+
...current,
|
|
198
|
+
...input,
|
|
199
|
+
id,
|
|
200
|
+
};
|
|
201
|
+
collection.items = collection.items.map((item) => item.id === id ? nextRecord : item);
|
|
202
|
+
return { ...nextRecord };
|
|
203
|
+
},
|
|
204
|
+
async delete(api, id) {
|
|
205
|
+
const collection = ensureMemoryCollection(api);
|
|
206
|
+
collection.items = collection.items.filter((item) => item.id !== id);
|
|
207
|
+
},
|
|
208
|
+
async post(_path, _input) {
|
|
209
|
+
throw new Error('Memory resource client does not support custom POST actions');
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function readGlobalResourceClient() {
|
|
214
|
+
if (typeof window === 'undefined')
|
|
215
|
+
return undefined;
|
|
216
|
+
const value = window.__RDSL_RESOURCE_CLIENT__;
|
|
217
|
+
return value && typeof value === 'object' ? value : undefined;
|
|
218
|
+
}
|
|
219
|
+
function readGlobalApiBase() {
|
|
220
|
+
if (typeof window === 'undefined')
|
|
221
|
+
return undefined;
|
|
222
|
+
const value = window.__RDSL_API_BASE__;
|
|
223
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
224
|
+
}
|
|
225
|
+
function getDefaultResourceClient() {
|
|
226
|
+
const globalBase = readGlobalApiBase();
|
|
227
|
+
if (globalBase) {
|
|
228
|
+
const existing = fetchClientCache.get(globalBase);
|
|
229
|
+
if (existing)
|
|
230
|
+
return existing;
|
|
231
|
+
const created = createFetchResourceClient({ baseUrl: globalBase });
|
|
232
|
+
fetchClientCache.set(globalBase, created);
|
|
233
|
+
return created;
|
|
234
|
+
}
|
|
235
|
+
if (typeof globalThis.fetch === 'function') {
|
|
236
|
+
if (!sameOriginFetchClient) {
|
|
237
|
+
sameOriginFetchClient = createFetchResourceClient();
|
|
238
|
+
}
|
|
239
|
+
return sameOriginFetchClient;
|
|
240
|
+
}
|
|
241
|
+
if (!defaultMemoryClient) {
|
|
242
|
+
defaultMemoryClient = createMemoryResourceClient();
|
|
243
|
+
}
|
|
244
|
+
return defaultMemoryClient;
|
|
245
|
+
}
|
|
246
|
+
export function ResourceProvider({ client, children }) {
|
|
247
|
+
return React.createElement(ResourceClientContext.Provider, { value: client }, children);
|
|
248
|
+
}
|
|
249
|
+
export function useResourceClient() {
|
|
250
|
+
return React.useContext(ResourceClientContext)
|
|
251
|
+
?? readGlobalResourceClient()
|
|
252
|
+
?? getDefaultResourceClient();
|
|
253
|
+
}
|
|
254
|
+
export function resetResourceClientTestState() {
|
|
255
|
+
memoryCollections.clear();
|
|
256
|
+
fetchClientCache.clear();
|
|
257
|
+
sameOriginFetchClient = null;
|
|
258
|
+
defaultMemoryClient = null;
|
|
259
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ResourceClient } from './resourceClient.js';
|
|
2
|
+
export interface ResourceStoreSnapshot<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}> {
|
|
5
|
+
items: T[];
|
|
6
|
+
error: unknown;
|
|
7
|
+
pendingCount: number;
|
|
8
|
+
resolvedOnce: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ResourceStore<T extends {
|
|
11
|
+
id: string;
|
|
12
|
+
}> {
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
getSnapshot(): ResourceStoreSnapshot<T>;
|
|
15
|
+
getById(id: string): T | undefined;
|
|
16
|
+
load(force?: boolean): Promise<T[]>;
|
|
17
|
+
createItem(input: Partial<T>): Promise<T>;
|
|
18
|
+
updateItem(id: string, input: Partial<T>): Promise<T>;
|
|
19
|
+
deleteItem(id: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function getResourceStore<T extends {
|
|
22
|
+
id: string;
|
|
23
|
+
}>(client: ResourceClient, api: string): ResourceStore<T>;
|
|
24
|
+
export declare function resetResourceStoreTestState(): void;
|
|
25
|
+
//# sourceMappingURL=resourceStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resourceStore.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,qBAAqB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC7D,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IACrD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,WAAW,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACpC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAkLD,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACvD,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,MAAM,GACV,aAAa,CAAC,CAAC,CAAC,CAOlB;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const resourceStores = new Map();
|
|
2
|
+
const resourceClientIds = new WeakMap();
|
|
3
|
+
let nextClientId = 1;
|
|
4
|
+
function storeKey(client, api) {
|
|
5
|
+
const resourceClient = client;
|
|
6
|
+
let id = resourceClientIds.get(resourceClient);
|
|
7
|
+
if (!id) {
|
|
8
|
+
id = nextClientId;
|
|
9
|
+
nextClientId += 1;
|
|
10
|
+
resourceClientIds.set(resourceClient, id);
|
|
11
|
+
}
|
|
12
|
+
return `${id}:${api}`;
|
|
13
|
+
}
|
|
14
|
+
function notifyState(state) {
|
|
15
|
+
for (const listener of state.listeners) {
|
|
16
|
+
listener();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function resourceLeaf(api) {
|
|
20
|
+
const parts = api.split('/').filter(Boolean);
|
|
21
|
+
return parts[parts.length - 1] ?? 'record';
|
|
22
|
+
}
|
|
23
|
+
function syncNextId(api, state) {
|
|
24
|
+
const prefix = `${resourceLeaf(api)}-`;
|
|
25
|
+
let max = 0;
|
|
26
|
+
for (const item of state.items) {
|
|
27
|
+
if (!item.id.startsWith(prefix))
|
|
28
|
+
continue;
|
|
29
|
+
const suffix = Number(item.id.slice(prefix.length));
|
|
30
|
+
if (Number.isFinite(suffix) && suffix > max) {
|
|
31
|
+
max = suffix;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
state.nextId = max + 1;
|
|
35
|
+
}
|
|
36
|
+
function createSyntheticId(api, state) {
|
|
37
|
+
const id = `${resourceLeaf(api)}-${state.nextId}`;
|
|
38
|
+
state.nextId += 1;
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
function createResourceStore(client, api) {
|
|
42
|
+
const state = {
|
|
43
|
+
items: [],
|
|
44
|
+
listeners: new Set(),
|
|
45
|
+
error: null,
|
|
46
|
+
pendingCount: 0,
|
|
47
|
+
resolvedOnce: false,
|
|
48
|
+
inFlightLoad: null,
|
|
49
|
+
nextId: 1,
|
|
50
|
+
};
|
|
51
|
+
async function runMutation(mutate) {
|
|
52
|
+
state.pendingCount += 1;
|
|
53
|
+
state.error = null;
|
|
54
|
+
notifyState(state);
|
|
55
|
+
try {
|
|
56
|
+
const result = await mutate();
|
|
57
|
+
state.resolvedOnce = true;
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
state.error = error;
|
|
62
|
+
state.resolvedOnce = true;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
state.pendingCount = Math.max(0, state.pendingCount - 1);
|
|
67
|
+
notifyState(state);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
subscribe(listener) {
|
|
72
|
+
state.listeners.add(listener);
|
|
73
|
+
return () => {
|
|
74
|
+
state.listeners.delete(listener);
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
getSnapshot() {
|
|
78
|
+
return {
|
|
79
|
+
items: state.items,
|
|
80
|
+
error: state.error,
|
|
81
|
+
pendingCount: state.pendingCount,
|
|
82
|
+
resolvedOnce: state.resolvedOnce,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
getById(id) {
|
|
86
|
+
return state.items.find((item) => item.id === id);
|
|
87
|
+
},
|
|
88
|
+
load(force = false) {
|
|
89
|
+
if (!force && state.resolvedOnce && !state.error) {
|
|
90
|
+
return Promise.resolve(state.items);
|
|
91
|
+
}
|
|
92
|
+
if (state.inFlightLoad) {
|
|
93
|
+
return state.inFlightLoad;
|
|
94
|
+
}
|
|
95
|
+
state.pendingCount += 1;
|
|
96
|
+
state.error = null;
|
|
97
|
+
notifyState(state);
|
|
98
|
+
state.inFlightLoad = client.list(api)
|
|
99
|
+
.then((items) => {
|
|
100
|
+
state.items = [...items];
|
|
101
|
+
state.error = null;
|
|
102
|
+
state.resolvedOnce = true;
|
|
103
|
+
syncNextId(api, state);
|
|
104
|
+
return state.items;
|
|
105
|
+
})
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
state.error = error;
|
|
108
|
+
state.resolvedOnce = true;
|
|
109
|
+
throw error;
|
|
110
|
+
})
|
|
111
|
+
.finally(() => {
|
|
112
|
+
state.pendingCount = Math.max(0, state.pendingCount - 1);
|
|
113
|
+
state.inFlightLoad = null;
|
|
114
|
+
notifyState(state);
|
|
115
|
+
});
|
|
116
|
+
return state.inFlightLoad;
|
|
117
|
+
},
|
|
118
|
+
createItem(input) {
|
|
119
|
+
return runMutation(async () => {
|
|
120
|
+
const created = await client.create(api, input);
|
|
121
|
+
const nextRecord = {
|
|
122
|
+
...created,
|
|
123
|
+
id: String(created.id ?? createSyntheticId(api, state)),
|
|
124
|
+
};
|
|
125
|
+
state.items = [...state.items, nextRecord];
|
|
126
|
+
syncNextId(api, state);
|
|
127
|
+
return nextRecord;
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
updateItem(id, input) {
|
|
131
|
+
return runMutation(async () => {
|
|
132
|
+
const current = state.items.find((item) => item.id === id);
|
|
133
|
+
const updated = await client.update(api, id, input);
|
|
134
|
+
const nextRecord = {
|
|
135
|
+
...current,
|
|
136
|
+
...input,
|
|
137
|
+
...updated,
|
|
138
|
+
id: String(updated.id ?? id),
|
|
139
|
+
};
|
|
140
|
+
state.items = state.items.map((item) => item.id === id ? nextRecord : item);
|
|
141
|
+
return nextRecord;
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
deleteItem(id) {
|
|
145
|
+
return runMutation(async () => {
|
|
146
|
+
await client.delete(api, id);
|
|
147
|
+
state.items = state.items.filter((item) => item.id !== id);
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function getResourceStore(client, api) {
|
|
153
|
+
const key = storeKey(client, api);
|
|
154
|
+
const existing = resourceStores.get(key);
|
|
155
|
+
if (existing)
|
|
156
|
+
return existing;
|
|
157
|
+
const created = createResourceStore(client, api);
|
|
158
|
+
resourceStores.set(key, created);
|
|
159
|
+
return created;
|
|
160
|
+
}
|
|
161
|
+
export function resetResourceStoreTestState() {
|
|
162
|
+
resourceStores.clear();
|
|
163
|
+
nextClientId = 1;
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resourceTarget.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceTarget.ts"],"names":[],"mappings":"AAUA,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAa3E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function normalizeTarget(target) {
|
|
2
|
+
return target.trim().replace(/^\/+|\/+$/g, '');
|
|
3
|
+
}
|
|
4
|
+
function apiLeaf(api) {
|
|
5
|
+
const normalized = normalizeTarget(api);
|
|
6
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
7
|
+
return parts[parts.length - 1] ?? normalized;
|
|
8
|
+
}
|
|
9
|
+
export function matchesResourceTarget(api, target) {
|
|
10
|
+
if (typeof target !== 'string' || target.trim() === '')
|
|
11
|
+
return false;
|
|
12
|
+
const normalizedTarget = normalizeTarget(target);
|
|
13
|
+
const normalizedApi = normalizeTarget(api);
|
|
14
|
+
const leaf = apiLeaf(api);
|
|
15
|
+
return [
|
|
16
|
+
normalizedApi,
|
|
17
|
+
leaf,
|
|
18
|
+
`${leaf}.list`,
|
|
19
|
+
`resource.${leaf}`,
|
|
20
|
+
`resource.${leaf}.list`,
|
|
21
|
+
].includes(normalizedTarget);
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AuthUser {
|
|
2
|
+
id?: string;
|
|
3
|
+
role?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface AuthState {
|
|
7
|
+
currentUser: AuthUser | null;
|
|
8
|
+
}
|
|
9
|
+
interface AuthProviderProps {
|
|
10
|
+
value: AuthState;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function AuthProvider({ value, children }: AuthProviderProps): any;
|
|
14
|
+
export declare function useAuth(): AuthState;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=useAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/hooks/useAuth.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC9B;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAiBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,iBAAiB,OAElE;AAED,wBAAgB,OAAO,IAAI,SAAS,CAEnC"}
|