@nubitio/hydra 0.1.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 +21 -0
- package/README.md +53 -0
- package/dist/index.cjs +862 -0
- package/dist/index.d.cts +336 -0
- package/dist/index.d.mts +336 -0
- package/dist/index.mjs +826 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _nubitio_core = require("@nubitio/core");
|
|
25
|
+
let react = require("react");
|
|
26
|
+
react = __toESM(react, 1);
|
|
27
|
+
let _nubitio_crud = require("@nubitio/crud");
|
|
28
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
let _tanstack_react_query = require("@tanstack/react-query");
|
|
30
|
+
//#region packages/hydra/HydraRemoteDataSource.ts
|
|
31
|
+
const defaultHttpClient = (0, _nubitio_core.createCoreHttpClient)({ baseUrl: "" });
|
|
32
|
+
function findPreloadedItem(loadOptions, idField, id) {
|
|
33
|
+
const prependData = Array.isArray(loadOptions.prependData) ? loadOptions.prependData : [];
|
|
34
|
+
const appendData = Array.isArray(loadOptions.appendData) ? loadOptions.appendData : [];
|
|
35
|
+
return [...prependData, ...appendData].find((item) => item[idField] === id || item["@id"] === id) ?? null;
|
|
36
|
+
}
|
|
37
|
+
function addIriField(url, item) {
|
|
38
|
+
const itemId = item["id"] ?? item["code"] ?? item["uuid"] ?? item["slug"];
|
|
39
|
+
const iri = typeof item["@id"] === "string" ? item["@id"] : typeof itemId === "string" || typeof itemId === "number" ? `${url}/${itemId}` : void 0;
|
|
40
|
+
return {
|
|
41
|
+
...item,
|
|
42
|
+
_iri: iri
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function normalizeFilterRules(filterRules, defaultFilterRules) {
|
|
46
|
+
const filters = [];
|
|
47
|
+
defaultFilterRules.forEach((rule) => filters.push(rule));
|
|
48
|
+
if (filterRules.length === 3 && typeof filterRules[0] !== "object") filters.push([
|
|
49
|
+
filterRules[0],
|
|
50
|
+
filterRules[1],
|
|
51
|
+
filterRules[2]
|
|
52
|
+
]);
|
|
53
|
+
else filterRules.forEach((rule) => {
|
|
54
|
+
filters.push(Array.isArray(rule) ? rule : [rule]);
|
|
55
|
+
});
|
|
56
|
+
return filters;
|
|
57
|
+
}
|
|
58
|
+
function applyLoadOptionDefaults(loadOptions, options) {
|
|
59
|
+
const nextOptions = { ...loadOptions };
|
|
60
|
+
options.forEach((option) => {
|
|
61
|
+
for (const key in option) if (Object.prototype.hasOwnProperty.call(option, key)) nextOptions[key] = option[key];
|
|
62
|
+
});
|
|
63
|
+
return nextOptions;
|
|
64
|
+
}
|
|
65
|
+
function normalizeSortRules(sortRules, defaultSortRules, idField) {
|
|
66
|
+
const [firstSort] = sortRules;
|
|
67
|
+
const firstSelector = typeof firstSort === "string" ? firstSort : firstSort?.selector;
|
|
68
|
+
if (sortRules.length === 1 && firstSelector === idField && defaultSortRules.length === 0) return [];
|
|
69
|
+
if (sortRules.length === 1 && firstSelector === idField) return [...defaultSortRules];
|
|
70
|
+
return sortRules;
|
|
71
|
+
}
|
|
72
|
+
function stripAdapterOnlyParams(loadOptions) {
|
|
73
|
+
const nextOptions = { ...loadOptions };
|
|
74
|
+
delete nextOptions.searchOperation;
|
|
75
|
+
if (nextOptions.searchExpr === void 0) delete nextOptions.searchValue;
|
|
76
|
+
return nextOptions;
|
|
77
|
+
}
|
|
78
|
+
function convertPaginationParams(loadOptions) {
|
|
79
|
+
const nextOptions = { ...loadOptions };
|
|
80
|
+
const take = typeof nextOptions.take === "number" ? nextOptions.take : void 0;
|
|
81
|
+
const skip = typeof nextOptions.skip === "number" ? nextOptions.skip : void 0;
|
|
82
|
+
if (take !== void 0) {
|
|
83
|
+
nextOptions.itemsPerPage = take;
|
|
84
|
+
delete nextOptions.take;
|
|
85
|
+
}
|
|
86
|
+
if (take !== void 0 && skip !== void 0) {
|
|
87
|
+
nextOptions.page = Math.floor(skip / take) + 1;
|
|
88
|
+
delete nextOptions.skip;
|
|
89
|
+
} else if (skip !== void 0) delete nextOptions.skip;
|
|
90
|
+
return nextOptions;
|
|
91
|
+
}
|
|
92
|
+
function getFlatIriFilter(filter) {
|
|
93
|
+
if (!Array.isArray(filter)) return null;
|
|
94
|
+
return filter.length === 1 && Array.isArray(filter[0]) ? filter[0] : filter;
|
|
95
|
+
}
|
|
96
|
+
function parseCollectionResponse(responseData, responseHeaders) {
|
|
97
|
+
if (Array.isArray(responseData)) {
|
|
98
|
+
const headerCount = Number(responseHeaders.get("x-total-count"));
|
|
99
|
+
return {
|
|
100
|
+
data: responseData,
|
|
101
|
+
totalCount: Number.isFinite(headerCount) ? headerCount : responseData.length
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (responseData && typeof responseData === "object") {
|
|
105
|
+
const body = responseData;
|
|
106
|
+
const member = body["hydra:member"] ?? body["member"];
|
|
107
|
+
if (Array.isArray(member)) {
|
|
108
|
+
const rawTotal = body["hydra:totalItems"] ?? body["totalItems"] ?? member.length;
|
|
109
|
+
const totalCount = Number(rawTotal);
|
|
110
|
+
return {
|
|
111
|
+
data: member,
|
|
112
|
+
totalCount: Number.isFinite(totalCount) ? totalCount : member.length
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
data: [],
|
|
118
|
+
totalCount: 0
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
var HydraRemoteDataSource = class {
|
|
122
|
+
config;
|
|
123
|
+
httpClient;
|
|
124
|
+
constructor(config) {
|
|
125
|
+
this.config = config;
|
|
126
|
+
this.httpClient = config.httpClient ?? defaultHttpClient;
|
|
127
|
+
}
|
|
128
|
+
makeFilterRules(filterRules) {
|
|
129
|
+
return filterRules.map((filter) => `filter[]=["${filter.field}","${filter.operator}","${filter.value}"]`).join("&");
|
|
130
|
+
}
|
|
131
|
+
prepareLoadOptions(loadOptions) {
|
|
132
|
+
const withDefaults = applyLoadOptionDefaults(loadOptions, this.config.options ?? []);
|
|
133
|
+
const normalizedFilters = normalizeFilterRules(withDefaults.filter ?? [], this.config.defaultFilterRules ?? []);
|
|
134
|
+
return convertPaginationParams(stripAdapterOnlyParams({
|
|
135
|
+
...withDefaults,
|
|
136
|
+
filter: normalizedFilters,
|
|
137
|
+
sort: normalizeSortRules(withDefaults.sort ?? [], this.config.defaultSortRules ?? [], this.config.idField)
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
async load(loadOptions) {
|
|
141
|
+
const preparedOptions = this.prepareLoadOptions(loadOptions);
|
|
142
|
+
if (this.config.iriMode) {
|
|
143
|
+
const flatFilter = getFlatIriFilter(preparedOptions.filter);
|
|
144
|
+
if (Array.isArray(flatFilter) && flatFilter.length === 3 && flatFilter[0] === "_iri" && flatFilter[1] === "=" && typeof flatFilter[2] === "string") {
|
|
145
|
+
const item = await this.byKey(flatFilter[2]);
|
|
146
|
+
return {
|
|
147
|
+
data: item ? [addIriField(this.config.url, item)] : [],
|
|
148
|
+
totalCount: item ? 1 : 0,
|
|
149
|
+
summary: null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const result = await this.fetchAll(preparedOptions);
|
|
153
|
+
return {
|
|
154
|
+
...result,
|
|
155
|
+
data: result.data.map((item) => addIriField(this.config.url, item))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return this.fetchAll(preparedOptions);
|
|
159
|
+
}
|
|
160
|
+
async byKey(key) {
|
|
161
|
+
const id = typeof key === "object" && key !== null ? key[this.config.idField] : key;
|
|
162
|
+
if (id === void 0) return null;
|
|
163
|
+
const loadOptions = applyLoadOptionDefaults({}, this.config.options ?? []);
|
|
164
|
+
const item = await this.fetchById(this.config.byKeyUrl ?? this.config.url, id, loadOptions);
|
|
165
|
+
return this.config.iriMode && item ? addIriField(this.config.url, item) : item;
|
|
166
|
+
}
|
|
167
|
+
async fetchAll(loadOptions) {
|
|
168
|
+
const response = await this.httpClient.get(this.config.url, { params: loadOptions });
|
|
169
|
+
const parsed = parseCollectionResponse(response.data, response.headers);
|
|
170
|
+
let data = parsed.data;
|
|
171
|
+
if (loadOptions.appendData) data = [...data, ...loadOptions.appendData];
|
|
172
|
+
if (loadOptions.prependData) data = [...loadOptions.prependData, ...data];
|
|
173
|
+
const totalCount = parsed.totalCount === parsed.data.length ? data.length : parsed.totalCount;
|
|
174
|
+
return {
|
|
175
|
+
data,
|
|
176
|
+
totalCount,
|
|
177
|
+
summary: null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async fetchById(url, id, loadOptions) {
|
|
181
|
+
const preloadedItem = findPreloadedItem(loadOptions, this.config.idField, id);
|
|
182
|
+
if (preloadedItem) return preloadedItem;
|
|
183
|
+
const resolvedUrl = typeof id === "string" && id.startsWith("/") ? id : `${url}/${id}`;
|
|
184
|
+
return this.httpClient.get(resolvedUrl).then((response) => response.data).catch(() => findPreloadedItem(loadOptions, this.config.idField, id));
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const createHydraResourceStore = (options) => new HydraRemoteDataSource(options);
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region packages/hydra/HydraResourceStoreProvider.tsx
|
|
190
|
+
function HydraResourceStoreProvider({ children }) {
|
|
191
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_crud.ResourceStoreProvider, {
|
|
192
|
+
factory: createHydraResourceStore,
|
|
193
|
+
children
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region packages/hydra/openApiParser.ts
|
|
198
|
+
/**
|
|
199
|
+
* Converts a camelCase or PascalCase name to dash-case.
|
|
200
|
+
* Mirrors API Platform's DashPathSegmentNameGenerator behaviour.
|
|
201
|
+
* Examples:
|
|
202
|
+
* "Category" → "category"
|
|
203
|
+
* "SunatCatalog" → "sunat-catalog"
|
|
204
|
+
* "CashMovementCategory" → "cash-movement-category"
|
|
205
|
+
* "cashMovementCategory" → "cash-movement-category"
|
|
206
|
+
*/
|
|
207
|
+
function toDashCase(name) {
|
|
208
|
+
return name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Pluralizes an English word (snake_case or lowercase) with common rules.
|
|
212
|
+
*
|
|
213
|
+
* - Words ending in a consonant + 'y' → replace 'y' with 'ies'
|
|
214
|
+
* category → categories, company → companies
|
|
215
|
+
* - Words ending in s/x/z/ch/sh → append 'es'
|
|
216
|
+
* branch → branches, box → boxes
|
|
217
|
+
* - Everything else → append 's'
|
|
218
|
+
* warehouse → warehouses, product → products
|
|
219
|
+
*/
|
|
220
|
+
function pluralize(word) {
|
|
221
|
+
if (word.endsWith("y") && ![
|
|
222
|
+
"ay",
|
|
223
|
+
"ey",
|
|
224
|
+
"iy",
|
|
225
|
+
"oy",
|
|
226
|
+
"uy"
|
|
227
|
+
].some((v) => word.endsWith(v))) return word.slice(0, -1) + "ies";
|
|
228
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) return word + "es";
|
|
229
|
+
return word + "s";
|
|
230
|
+
}
|
|
231
|
+
/** Entrypoint property names that are already in plural form (dash-case). */
|
|
232
|
+
const ALREADY_PLURAL = new Set([
|
|
233
|
+
"purchases",
|
|
234
|
+
"sales",
|
|
235
|
+
"invoices",
|
|
236
|
+
"transfers",
|
|
237
|
+
"movements",
|
|
238
|
+
"shifts",
|
|
239
|
+
"outbounds",
|
|
240
|
+
"cash-movements",
|
|
241
|
+
"cash-registers",
|
|
242
|
+
"cash-shifts"
|
|
243
|
+
]);
|
|
244
|
+
/**
|
|
245
|
+
* Derives the API URL from an Entrypoint property ID.
|
|
246
|
+
* "#Entrypoint/branch" -> "/api/branches"
|
|
247
|
+
* "#Entrypoint/cashMovementCategory" -> "/api/cash-movement-categories"
|
|
248
|
+
*/
|
|
249
|
+
function deriveApiUrl(entrypointPropertyId) {
|
|
250
|
+
const dashed = toDashCase(entrypointPropertyId.split("/").pop() ?? "");
|
|
251
|
+
return `/api/${ALREADY_PLURAL.has(dashed) ? dashed : pluralize(dashed)}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Safely extracts a string range from whatever `property.range` actually is.
|
|
255
|
+
*
|
|
256
|
+
* Real API Platform responses can return:
|
|
257
|
+
* - a plain string: "xmls:string", "#Company"
|
|
258
|
+
* - an IRI object: { "@id": "http://www.w3.org/2001/XMLSchema#string" }
|
|
259
|
+
* - null or undefined
|
|
260
|
+
* - a Hydra collection range array where the resource class lives under
|
|
261
|
+
* `owl:equivalentClass.owl:allValuesFrom.@id`
|
|
262
|
+
*/
|
|
263
|
+
function normalizeRange(raw) {
|
|
264
|
+
if (typeof raw === "string") return raw;
|
|
265
|
+
if (Array.isArray(raw)) for (const item of raw) {
|
|
266
|
+
if (!item || typeof item !== "object") continue;
|
|
267
|
+
const equivalentClass = item["owl:equivalentClass"];
|
|
268
|
+
if (!equivalentClass || typeof equivalentClass !== "object") continue;
|
|
269
|
+
const allValuesFrom = equivalentClass["owl:allValuesFrom"];
|
|
270
|
+
if (!allValuesFrom || typeof allValuesFrom !== "object") continue;
|
|
271
|
+
const id = allValuesFrom["@id"];
|
|
272
|
+
if (typeof id === "string") return id;
|
|
273
|
+
}
|
|
274
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
275
|
+
const id = raw["@id"];
|
|
276
|
+
if (typeof id === "string") return id;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Convert an OpenAPI property descriptor to a short-form xmls: range string.
|
|
281
|
+
*/
|
|
282
|
+
function mapOpenApiTypeToRange(prop) {
|
|
283
|
+
if (prop.format === "date-time") return "xmls:dateTime";
|
|
284
|
+
switch (prop.type) {
|
|
285
|
+
case "integer": return "xmls:integer";
|
|
286
|
+
case "number": return "xmls:decimal";
|
|
287
|
+
case "boolean": return "xmls:boolean";
|
|
288
|
+
case "string": return "xmls:string";
|
|
289
|
+
default: return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Extracts the technical property name from a HydraSupportedProperty.
|
|
294
|
+
*
|
|
295
|
+
* The technical name (used in API JSON payloads) is found in sp.property['@id'],
|
|
296
|
+
* which looks like "#Branch/businessName" or "#Category/name".
|
|
297
|
+
* We extract the part after the last '/'.
|
|
298
|
+
*
|
|
299
|
+
* Fallback chain: @id suffix → property.label → title → ''
|
|
300
|
+
*/
|
|
301
|
+
function technicalName(sp) {
|
|
302
|
+
const fromId = sp.property?.["@id"]?.split("/").pop();
|
|
303
|
+
if (fromId && fromId !== "") return fromId;
|
|
304
|
+
return sp.property?.label ?? sp.title ?? "";
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Extract `hydra:search` mappings from a HydraClass.
|
|
308
|
+
* Returns an empty array if the class has no `hydra:search` block,
|
|
309
|
+
* or if any mapping entry is missing the required `hydra:property` field.
|
|
310
|
+
* Never throws.
|
|
311
|
+
*/
|
|
312
|
+
function extractSearchMappings(cls) {
|
|
313
|
+
const searchBlock = cls["hydra:search"];
|
|
314
|
+
if (!searchBlock) return [];
|
|
315
|
+
const mappings = searchBlock["hydra:mapping"] ?? [];
|
|
316
|
+
const result = [];
|
|
317
|
+
for (const mapping of mappings) {
|
|
318
|
+
const property = mapping["hydra:property"];
|
|
319
|
+
const variable = mapping["hydra:variable"];
|
|
320
|
+
if (!property || !variable) continue;
|
|
321
|
+
result.push({
|
|
322
|
+
property,
|
|
323
|
+
variable,
|
|
324
|
+
required: mapping["hydra:required"] ?? false
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Extract the HTTP method names from `supportedOperation` on a HydraClass.
|
|
331
|
+
* Returns an uppercase string array, e.g. ['GET', 'POST', 'PATCH', 'DELETE'].
|
|
332
|
+
* Returns an empty array when the class has no `supportedOperation` block.
|
|
333
|
+
*
|
|
334
|
+
* Wire format (confirmed from /api/docs.jsonld): compact key `supportedOperation`
|
|
335
|
+
* (no `hydra:` prefix). API Platform compact context strips the namespace prefix.
|
|
336
|
+
*
|
|
337
|
+
* Accepts both `hydra:method` and `method` field names inside each operation entry
|
|
338
|
+
* to be safe across different API Platform serialisation variants.
|
|
339
|
+
*/
|
|
340
|
+
function extractSupportedOperations(cls) {
|
|
341
|
+
const operations = cls["supportedOperation"];
|
|
342
|
+
if (!operations || operations.length === 0) return [];
|
|
343
|
+
return operations.map((op) => (op["hydra:method"] ?? op["method"] ?? "").toUpperCase()).filter(Boolean);
|
|
344
|
+
}
|
|
345
|
+
function extractEntrypointCollectionOperations(doc) {
|
|
346
|
+
const entrypoint = doc["supportedClass"].find((c) => c["@id"] === "#Entrypoint");
|
|
347
|
+
if (!entrypoint) return {};
|
|
348
|
+
const collectionOperations = {};
|
|
349
|
+
for (const sp of entrypoint.supportedProperty ?? []) {
|
|
350
|
+
const rawRange = sp.property?.range;
|
|
351
|
+
const range = normalizeRange(rawRange);
|
|
352
|
+
if (!range) continue;
|
|
353
|
+
const className = range.replace("#", "");
|
|
354
|
+
collectionOperations[className] = (sp.property?.supportedOperation ?? []).map((op) => (op["hydra:method"] ?? op["method"] ?? "").toUpperCase()).filter(Boolean);
|
|
355
|
+
}
|
|
356
|
+
return collectionOperations;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Parse a raw `/api/docs.jsonld` Hydra JSON-LD response into a typed
|
|
360
|
+
* Record<className, HydraResourceSchema>.
|
|
361
|
+
*
|
|
362
|
+
* @throws Error if doc is not a valid Hydra ApiDocumentation.
|
|
363
|
+
*/
|
|
364
|
+
function parseHydraDoc(doc) {
|
|
365
|
+
const result = {};
|
|
366
|
+
const collectionOperationsMap = extractEntrypointCollectionOperations(doc);
|
|
367
|
+
const urlMap = {};
|
|
368
|
+
const entrypoint = doc["supportedClass"].find((c) => c["@id"] === "#Entrypoint");
|
|
369
|
+
if (entrypoint) for (const sp of entrypoint.supportedProperty ?? []) {
|
|
370
|
+
const rawRange = sp.property?.range;
|
|
371
|
+
const range = normalizeRange(rawRange);
|
|
372
|
+
const propId = sp.property?.["@id"];
|
|
373
|
+
if (range && propId) {
|
|
374
|
+
const className = range.replace("#", "");
|
|
375
|
+
urlMap[className] = deriveApiUrl(propId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const cls of doc["supportedClass"]) {
|
|
379
|
+
if (cls["@id"] === "#Entrypoint") continue;
|
|
380
|
+
const className = cls["@id"].replace("#", "");
|
|
381
|
+
const fields = [];
|
|
382
|
+
for (const sp of cls.supportedProperty ?? []) {
|
|
383
|
+
const readable = sp.readable !== false;
|
|
384
|
+
const writeable = sp.writeable !== false;
|
|
385
|
+
if (!readable) continue;
|
|
386
|
+
fields.push({
|
|
387
|
+
name: technicalName(sp),
|
|
388
|
+
range: normalizeRange(sp.property?.range),
|
|
389
|
+
propertyType: sp.property?.["@type"] ?? "rdf:Property",
|
|
390
|
+
required: sp.required ?? false,
|
|
391
|
+
readable,
|
|
392
|
+
writeable,
|
|
393
|
+
"hydra:title": sp["title"] ?? sp["hydra:title"] ?? void 0,
|
|
394
|
+
crudHints: sp["x-crud"]
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
result[className] = {
|
|
398
|
+
className,
|
|
399
|
+
apiUrl: urlMap[className] ?? `/api/${pluralize(toDashCase(className))}`,
|
|
400
|
+
fields,
|
|
401
|
+
searchMappings: extractSearchMappings(cls),
|
|
402
|
+
supportedOperations: Array.from(new Set([...collectionOperationsMap[className] ?? [], ...extractSupportedOperations(cls)]))
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Parse a raw `/api/docs.json` OpenAPI 3.1 response into the same
|
|
409
|
+
* Record<className, HydraResourceSchema> output shape so that
|
|
410
|
+
* `useResourceSchema` can consume it transparently.
|
|
411
|
+
*/
|
|
412
|
+
function parseOpenApiDoc(doc) {
|
|
413
|
+
const result = {};
|
|
414
|
+
for (const [className, schema] of Object.entries(doc.components?.schemas ?? {})) {
|
|
415
|
+
const fields = [];
|
|
416
|
+
const required = new Set(schema.required ?? []);
|
|
417
|
+
for (const [fieldName, prop] of Object.entries(schema.properties ?? {})) {
|
|
418
|
+
const readable = !prop.writeOnly;
|
|
419
|
+
const writeable = !prop.readOnly;
|
|
420
|
+
if (!readable) continue;
|
|
421
|
+
fields.push({
|
|
422
|
+
name: fieldName,
|
|
423
|
+
range: mapOpenApiTypeToRange(prop),
|
|
424
|
+
propertyType: "rdf:Property",
|
|
425
|
+
required: required.has(fieldName),
|
|
426
|
+
readable,
|
|
427
|
+
writeable
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
result[className] = {
|
|
431
|
+
className,
|
|
432
|
+
apiUrl: `/api/${pluralize(toDashCase(className))}`,
|
|
433
|
+
fields
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region packages/hydra/HydraToFieldMapper.ts
|
|
440
|
+
/**
|
|
441
|
+
* Returns true when the given `range` string declares an XSD integer type.
|
|
442
|
+
* Supports both short-form `xmls:` prefixes and full XSD IRIs.
|
|
443
|
+
*
|
|
444
|
+
* Used to decide whether the `id` field should be a `numberField()` (integer
|
|
445
|
+
* primary key) or a `textField()` (UUID / string primary key — safe default).
|
|
446
|
+
*/
|
|
447
|
+
function isIntegerRange(range) {
|
|
448
|
+
if (!range) return false;
|
|
449
|
+
if (range.startsWith("http://www.w3.org/2001/XMLSchema#")) {
|
|
450
|
+
const local = range.split("#").pop() ?? "";
|
|
451
|
+
return local === "integer" || local === "int" || local === "long";
|
|
452
|
+
}
|
|
453
|
+
return range === "xmls:integer" || range === "xmls:int" || range === "xmls:long" || range === "xsd:integer" || range === "xsd:int" || range === "xsd:long";
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Convert a camelCase or snake_case field name to a human-readable label.
|
|
457
|
+
* Examples:
|
|
458
|
+
* "firstName" → "First Name"
|
|
459
|
+
* "createdAt" → "Created At"
|
|
460
|
+
* "x_resource" → "X Resource"
|
|
461
|
+
*/
|
|
462
|
+
function toLabel(name) {
|
|
463
|
+
return name.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
|
|
464
|
+
}
|
|
465
|
+
function resolveRangeTag(range, propertyType) {
|
|
466
|
+
if (!range) return "text";
|
|
467
|
+
if (range.startsWith("http://www.w3.org/2001/XMLSchema#")) {
|
|
468
|
+
const local = range.split("#").pop() ?? "";
|
|
469
|
+
if (local === "boolean") return "boolean";
|
|
470
|
+
if (local === "dateTime") return "dateTime";
|
|
471
|
+
if (local === "integer") return "integer";
|
|
472
|
+
if (local === "decimal" || local === "float" || local === "double") return "decimal";
|
|
473
|
+
if (local === "string") return "text";
|
|
474
|
+
return "text";
|
|
475
|
+
}
|
|
476
|
+
if (range === "xsd:boolean") return "boolean";
|
|
477
|
+
if (range === "xsd:dateTime" || range === "xsd:date") return "dateTime";
|
|
478
|
+
if (range === "xsd:integer" || range === "xsd:int" || range === "xsd:long") return "integer";
|
|
479
|
+
if (range === "xsd:decimal" || range === "xsd:float" || range === "xsd:double") return "decimal";
|
|
480
|
+
if (range === "xsd:string") return "text";
|
|
481
|
+
if (range === "xmls:boolean") return "boolean";
|
|
482
|
+
if (range === "xmls:dateTime") return "dateTime";
|
|
483
|
+
if (range === "xmls:integer") return "integer";
|
|
484
|
+
if (range === "xmls:decimal") return "decimal";
|
|
485
|
+
if (range === "xmls:string") return "text";
|
|
486
|
+
if (range.startsWith("#")) return "entity";
|
|
487
|
+
if (range.startsWith("http")) return "entity";
|
|
488
|
+
if (propertyType === "Link") return "entity";
|
|
489
|
+
return "text";
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Apply `x-crud` backend hints on top of an already-built Field.
|
|
493
|
+
*
|
|
494
|
+
* Hints override inferred values; inference is the fallback when hints are absent.
|
|
495
|
+
* Only properties that are explicitly set (`!== undefined`) are applied.
|
|
496
|
+
*
|
|
497
|
+
* `hidden: true` sets `visible: false` so the column is hidden in the grid.
|
|
498
|
+
* `order` maps to the `Field.order` property used by native grid column ordering.
|
|
499
|
+
*/
|
|
500
|
+
function applyCrudHints(field, hints) {
|
|
501
|
+
if (!hints) return;
|
|
502
|
+
if (hints.filterable !== void 0) field.filterable = hints.filterable;
|
|
503
|
+
if (hints.sortable !== void 0) field.sortable = hints.sortable;
|
|
504
|
+
if (hints.hidden !== void 0 && hints.hidden) field.visible = false;
|
|
505
|
+
if (hints.order !== void 0) field.order = hints.order;
|
|
506
|
+
if (hints.width !== void 0) field.width = hints.width;
|
|
507
|
+
}
|
|
508
|
+
function resolveEntityValueField(relatedSchema) {
|
|
509
|
+
if (!relatedSchema) return "_iri";
|
|
510
|
+
const fieldNames = new Set(relatedSchema.fields.map((field) => field.name));
|
|
511
|
+
if (fieldNames.has("id") || fieldNames.has("@id")) return "_iri";
|
|
512
|
+
return "_iri";
|
|
513
|
+
}
|
|
514
|
+
function isStringLikeField(field) {
|
|
515
|
+
return field.range === void 0 || field.range === "xmls:string" || field.range === "xsd:string";
|
|
516
|
+
}
|
|
517
|
+
function resolveEntityTextField(relatedSchema, valueField) {
|
|
518
|
+
if (!relatedSchema) return "name";
|
|
519
|
+
for (const fieldName of [
|
|
520
|
+
"name",
|
|
521
|
+
"businessName",
|
|
522
|
+
"description",
|
|
523
|
+
"fullNumber",
|
|
524
|
+
"title",
|
|
525
|
+
"series"
|
|
526
|
+
]) {
|
|
527
|
+
const match = relatedSchema.fields.find((field) => field.name === fieldName && isStringLikeField(field));
|
|
528
|
+
if (match) return match.name;
|
|
529
|
+
}
|
|
530
|
+
const firstStringField = relatedSchema.fields.find((field) => field.name !== valueField && isStringLikeField(field));
|
|
531
|
+
if (firstStringField) return firstStringField.name;
|
|
532
|
+
return valueField;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Map a single HydraResourceSchema to an array of Field objects using the
|
|
536
|
+
* existing field builder utilities.
|
|
537
|
+
*
|
|
538
|
+
* Mapping rules (in priority order):
|
|
539
|
+
* 1. name === 'id' or name === '@id' → always emit as a hidden identity field
|
|
540
|
+
* (visible: false, readonly: true, isIdentity: true)
|
|
541
|
+
* Required so native grids and forms always have a stable row key.
|
|
542
|
+
* 2. readable: false → skip (already filtered upstream, but defensive)
|
|
543
|
+
* 3. writeable: false (display-only) → noneField
|
|
544
|
+
* 4. range resolves to 'boolean' → switchField
|
|
545
|
+
* 5. range resolves to 'dateTime' → datetimeField
|
|
546
|
+
* 6. range resolves to 'integer' → numberField with precision(0)
|
|
547
|
+
* 7. range resolves to 'decimal' → numberField
|
|
548
|
+
* 8. range resolves to 'entity' OR propertyType === 'Link' → entityField
|
|
549
|
+
* 9. range resolves to 'text' OR fallback → textField
|
|
550
|
+
*
|
|
551
|
+
* For each field, `filterable` is `true` when the field's property name appears in
|
|
552
|
+
* `schema.searchMappings` (from `hydra:search`). When `searchMappings` is empty
|
|
553
|
+
* (e.g. the resource uses a catch-all data-grid filter that doesn't
|
|
554
|
+
* enumerate field-level mappings), ALL fields default to `filterable: true`.
|
|
555
|
+
*
|
|
556
|
+
* After processing all schema fields, if no `id` field was found in the schema,
|
|
557
|
+
* a synthetic hidden identity field is injected so the grid always has a key.
|
|
558
|
+
*
|
|
559
|
+
* @param schema - The Hydra resource schema to map.
|
|
560
|
+
* @param urlLookup - Optional lookup function to resolve the API URL for a related
|
|
561
|
+
* entity class. When provided, Rule 8 uses this before falling back to the
|
|
562
|
+
* automatic pluralization heuristic. Example: `(cls) => resourceMap[cls]?.apiUrl`
|
|
563
|
+
*
|
|
564
|
+
* @pure — no React, no hooks, no side effects.
|
|
565
|
+
*/
|
|
566
|
+
function mapHydraSchemaToFields(schema, urlLookup, schemaLookup) {
|
|
567
|
+
const fields = [];
|
|
568
|
+
const filterableProperties = new Set((schema.searchMappings ?? []).map((m) => m.property));
|
|
569
|
+
let hasIdField = false;
|
|
570
|
+
for (const fieldSchema of schema.fields) {
|
|
571
|
+
const { name, range, propertyType, required, readable, writeable } = fieldSchema;
|
|
572
|
+
if (name === "id" || name === "@id") {
|
|
573
|
+
hasIdField = true;
|
|
574
|
+
const idField = {
|
|
575
|
+
...isIntegerRange(range) ? (0, _nubitio_crud.numberField)().name("id").label("Id").required(false).precision(0).build() : (0, _nubitio_crud.textField)().name("id").label("Id").required(false).build(),
|
|
576
|
+
isIdentity: true,
|
|
577
|
+
visible: false,
|
|
578
|
+
readonly: true,
|
|
579
|
+
filterable: false,
|
|
580
|
+
sortable: false,
|
|
581
|
+
hideable: false,
|
|
582
|
+
visibleOnForm: false
|
|
583
|
+
};
|
|
584
|
+
fields.push(idField);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (!readable) continue;
|
|
588
|
+
const hydraTitle = fieldSchema["hydra:title"];
|
|
589
|
+
const label = hydraTitle && hydraTitle.trim() !== "" ? hydraTitle : toLabel(name);
|
|
590
|
+
const filterable = filterableProperties.size === 0 ? true : filterableProperties.has(name);
|
|
591
|
+
if (!writeable) {
|
|
592
|
+
const field = {
|
|
593
|
+
...(0, _nubitio_crud.noneField)().name(name).label(label).required(required).build(),
|
|
594
|
+
filterable
|
|
595
|
+
};
|
|
596
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
597
|
+
fields.push(field);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const tag = resolveRangeTag(range, propertyType);
|
|
601
|
+
if (tag === "boolean") {
|
|
602
|
+
const field = {
|
|
603
|
+
...(0, _nubitio_crud.switchField)().name(name).label(label).required(required).build(),
|
|
604
|
+
filterable
|
|
605
|
+
};
|
|
606
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
607
|
+
fields.push(field);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (tag === "dateTime") {
|
|
611
|
+
const field = {
|
|
612
|
+
...(0, _nubitio_crud.datetimeField)().name(name).label(label).required(required).build(),
|
|
613
|
+
filterable
|
|
614
|
+
};
|
|
615
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
616
|
+
fields.push(field);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (tag === "integer") {
|
|
620
|
+
const field = {
|
|
621
|
+
...(0, _nubitio_crud.numberField)().name(name).label(label).required(required).precision(0).build(),
|
|
622
|
+
filterable
|
|
623
|
+
};
|
|
624
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
625
|
+
fields.push(field);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (tag === "decimal") {
|
|
629
|
+
const field = {
|
|
630
|
+
...(0, _nubitio_crud.numberField)().name(name).label(label).required(required).build(),
|
|
631
|
+
filterable
|
|
632
|
+
};
|
|
633
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
634
|
+
fields.push(field);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (tag === "entity" || propertyType === "Link") {
|
|
638
|
+
const resourceClass = range ? range.replace("#", "") : "";
|
|
639
|
+
const relatedSchema = resourceClass ? schemaLookup?.(resourceClass) : void 0;
|
|
640
|
+
const url = resourceClass ? urlLookup?.(resourceClass) ?? `/api/${pluralize(toDashCase(resourceClass))}` : "";
|
|
641
|
+
const valueField = resolveEntityValueField(relatedSchema);
|
|
642
|
+
const field = {
|
|
643
|
+
...(0, _nubitio_crud.entityField)(url, valueField, resolveEntityTextField(relatedSchema, valueField)).name(name).label(label).required(required).build(),
|
|
644
|
+
filterable
|
|
645
|
+
};
|
|
646
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
647
|
+
fields.push(field);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const field = {
|
|
651
|
+
...(0, _nubitio_crud.textField)().name(name).label(label).required(required).build(),
|
|
652
|
+
filterable
|
|
653
|
+
};
|
|
654
|
+
applyCrudHints(field, fieldSchema.crudHints);
|
|
655
|
+
fields.push(field);
|
|
656
|
+
}
|
|
657
|
+
if (!hasIdField) {
|
|
658
|
+
const syntheticId = {
|
|
659
|
+
...(0, _nubitio_crud.textField)().name("id").label("Id").required(false).build(),
|
|
660
|
+
isIdentity: true,
|
|
661
|
+
visible: false,
|
|
662
|
+
readonly: true,
|
|
663
|
+
filterable: false,
|
|
664
|
+
sortable: false,
|
|
665
|
+
hideable: false,
|
|
666
|
+
visibleOnForm: false
|
|
667
|
+
};
|
|
668
|
+
fields.unshift(syntheticId);
|
|
669
|
+
}
|
|
670
|
+
return fields;
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region packages/hydra/useHydraMetadata.ts
|
|
674
|
+
const JSONLD_URL = "/api/docs.jsonld";
|
|
675
|
+
const JSON_URL = "/api/docs.json";
|
|
676
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
677
|
+
/**
|
|
678
|
+
* Try to fetch and validate a Hydra JSON-LD doc.
|
|
679
|
+
* Returns the typed HydraApiDoc on success, or throws on failure.
|
|
680
|
+
*/
|
|
681
|
+
async function fetchHydraDoc(httpClient, url) {
|
|
682
|
+
const { data: json } = await httpClient.get(url);
|
|
683
|
+
const jsonType = json["@type"];
|
|
684
|
+
if (jsonType !== "ApiDocumentation" && jsonType !== "hydra:ApiDocumentation" || !Array.isArray(json["supportedClass"])) throw new Error("useHydraMetadata: response is not a valid Hydra ApiDocumentation");
|
|
685
|
+
return json;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Try to fetch and validate an OpenAPI 3.1 doc.
|
|
689
|
+
* Returns the typed OpenApiDoc on success, or throws on failure.
|
|
690
|
+
*/
|
|
691
|
+
async function fetchOpenApiDoc(httpClient, url) {
|
|
692
|
+
const { data: json } = await httpClient.get(url);
|
|
693
|
+
if (typeof json.openapi !== "string" || !json.openapi.startsWith("3")) throw new Error("useHydraMetadata: response is not a valid OpenAPI 3.x doc");
|
|
694
|
+
return json;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Stable query key used by useHydraMetadata.
|
|
698
|
+
* Export this so that consumers (e.g. SmartCrudPage retry button) can
|
|
699
|
+
* invalidate exactly the right query without coupling to an internal string.
|
|
700
|
+
*/
|
|
701
|
+
const API_DOC_QUERY_KEY = ["api-doc-discovery"];
|
|
702
|
+
/**
|
|
703
|
+
* Waterfall discovery:
|
|
704
|
+
* 1. Try /api/docs.jsonld (prefer Hydra — richer type info)
|
|
705
|
+
* 2. Fall back to /api/docs.json (OpenAPI 3.1)
|
|
706
|
+
* 3. Throw an Error if both fail so React Query sets isError = true
|
|
707
|
+
*/
|
|
708
|
+
async function fetchApiDoc(httpClient) {
|
|
709
|
+
try {
|
|
710
|
+
return {
|
|
711
|
+
format: "hydra",
|
|
712
|
+
doc: await fetchHydraDoc(httpClient, JSONLD_URL)
|
|
713
|
+
};
|
|
714
|
+
} catch {}
|
|
715
|
+
try {
|
|
716
|
+
return {
|
|
717
|
+
format: "openapi",
|
|
718
|
+
doc: await fetchOpenApiDoc(httpClient, JSON_URL)
|
|
719
|
+
};
|
|
720
|
+
} catch {}
|
|
721
|
+
throw new Error(`Failed to load API documentation from ${JSONLD_URL} and ${JSON_URL}. Make sure the API server is running.`);
|
|
722
|
+
}
|
|
723
|
+
function useHydraMetadata() {
|
|
724
|
+
const httpClient = (0, _nubitio_core.useCoreHttpClient)();
|
|
725
|
+
const { locale } = (0, _nubitio_core.useCoreConfig)();
|
|
726
|
+
const { data, error, isPending } = (0, _tanstack_react_query.useQuery)({
|
|
727
|
+
queryKey: [...API_DOC_QUERY_KEY, locale],
|
|
728
|
+
queryFn: () => fetchApiDoc(httpClient),
|
|
729
|
+
staleTime: isDev ? 0 : 5 * 6e4,
|
|
730
|
+
refetchOnWindowFocus: isDev,
|
|
731
|
+
refetchOnReconnect: isDev
|
|
732
|
+
});
|
|
733
|
+
return {
|
|
734
|
+
data,
|
|
735
|
+
isLoading: isPending,
|
|
736
|
+
error: error ?? void 0
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region packages/hydra/SchemaContext.tsx
|
|
741
|
+
/**
|
|
742
|
+
* SchemaContext — shared parsed schema, no per-page re-fetch.
|
|
743
|
+
*
|
|
744
|
+
* Provides a single shared `useHydraMetadata()` call at the provider level
|
|
745
|
+
* so that multiple `SmartCrudPage` instances mounted simultaneously share
|
|
746
|
+
* one `/api/docs.jsonld` request instead of each issuing their own.
|
|
747
|
+
*
|
|
748
|
+
* ## Usage
|
|
749
|
+
*
|
|
750
|
+
* ```tsx
|
|
751
|
+
* // In your app root (optional — SmartCrudPage still works without it):
|
|
752
|
+
* <SchemaProvider>
|
|
753
|
+
* <App />
|
|
754
|
+
* </SchemaProvider>
|
|
755
|
+
* ```
|
|
756
|
+
*
|
|
757
|
+
* ## Fallback behaviour
|
|
758
|
+
* If `useSchemaContext()` is called outside a `<SchemaProvider>`, it falls
|
|
759
|
+
* back to calling `useHydraMetadata()` directly, keeping backward compat.
|
|
760
|
+
*/
|
|
761
|
+
/** Sentinel value used to detect "outside a provider" */
|
|
762
|
+
const OUTSIDE_PROVIDER = Symbol("OUTSIDE_PROVIDER");
|
|
763
|
+
const SchemaContext = (0, react.createContext)(OUTSIDE_PROVIDER);
|
|
764
|
+
/**
|
|
765
|
+
* Wrap your app (or a subtree) with `SchemaProvider` to share a single
|
|
766
|
+
* `/api/docs.jsonld` fetch across all `SmartCrudPage` instances.
|
|
767
|
+
*/
|
|
768
|
+
function SchemaProvider({ children }) {
|
|
769
|
+
const metadata = useHydraMetadata();
|
|
770
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SchemaContext.Provider, {
|
|
771
|
+
value: metadata,
|
|
772
|
+
children
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Returns the shared schema context if inside a `<SchemaProvider>`,
|
|
777
|
+
* otherwise falls back to calling `useHydraMetadata()` directly.
|
|
778
|
+
*
|
|
779
|
+
* This makes `SmartCrudPage` work standalone without requiring a provider.
|
|
780
|
+
*/
|
|
781
|
+
function useSchemaContext() {
|
|
782
|
+
const ctx = (0, react.useContext)(SchemaContext);
|
|
783
|
+
const fallback = useHydraMetadata();
|
|
784
|
+
if (ctx === OUTSIDE_PROVIDER) return {
|
|
785
|
+
data: fallback.data,
|
|
786
|
+
isLoading: fallback.isLoading,
|
|
787
|
+
isError: fallback.error !== void 0,
|
|
788
|
+
error: fallback.error
|
|
789
|
+
};
|
|
790
|
+
return {
|
|
791
|
+
data: ctx.data,
|
|
792
|
+
isLoading: ctx.isLoading,
|
|
793
|
+
isError: ctx.error !== void 0,
|
|
794
|
+
error: ctx.error
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region packages/hydra/useResourceSchema.ts
|
|
799
|
+
/**
|
|
800
|
+
* Normalizes an API URL by stripping any leading slash and query string so that
|
|
801
|
+
* 'api/categories', '/api/categories', and '/api/categories?foo=bar' are treated as equal.
|
|
802
|
+
*/
|
|
803
|
+
function normalizeUrl(url) {
|
|
804
|
+
const base = url.split("?")[0];
|
|
805
|
+
return base.startsWith("/") ? base.slice(1) : base;
|
|
806
|
+
}
|
|
807
|
+
function useResourceSchema(apiUrl) {
|
|
808
|
+
const { data, isLoading, error } = useSchemaContext();
|
|
809
|
+
return (0, react.useMemo)(() => {
|
|
810
|
+
if (!data) return {
|
|
811
|
+
fields: [],
|
|
812
|
+
isLoading,
|
|
813
|
+
error,
|
|
814
|
+
supportedOperations: []
|
|
815
|
+
};
|
|
816
|
+
const resourceMap = data.format === "hydra" ? parseHydraDoc(data.doc) : parseOpenApiDoc(data.doc);
|
|
817
|
+
const normalizedInput = normalizeUrl(apiUrl);
|
|
818
|
+
const resourceSchema = Object.values(resourceMap).find((r) => normalizeUrl(r.apiUrl) === normalizedInput);
|
|
819
|
+
if (!resourceSchema) return {
|
|
820
|
+
fields: [],
|
|
821
|
+
isLoading: false,
|
|
822
|
+
error: /* @__PURE__ */ new Error(`No schema found for ${apiUrl} in API doc`),
|
|
823
|
+
supportedOperations: []
|
|
824
|
+
};
|
|
825
|
+
return {
|
|
826
|
+
fields: mapHydraSchemaToFields(resourceSchema, (className) => resourceMap[className]?.apiUrl, (className) => resourceMap[className]),
|
|
827
|
+
isLoading: false,
|
|
828
|
+
error: void 0,
|
|
829
|
+
supportedOperations: resourceSchema.supportedOperations ?? []
|
|
830
|
+
};
|
|
831
|
+
}, [
|
|
832
|
+
data,
|
|
833
|
+
isLoading,
|
|
834
|
+
error,
|
|
835
|
+
apiUrl
|
|
836
|
+
]);
|
|
837
|
+
}
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region packages/hydra/HydraResourceSchemaProvider.tsx
|
|
840
|
+
function useHydraResourceSchema({ apiUrl }) {
|
|
841
|
+
return useResourceSchema(apiUrl);
|
|
842
|
+
}
|
|
843
|
+
function HydraResourceSchemaProvider({ children }) {
|
|
844
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_crud.ResourceSchemaProvider, {
|
|
845
|
+
resolver: (0, react.useMemo)(() => ({ useResourceSchema: useHydraResourceSchema }), []),
|
|
846
|
+
children
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
//#endregion
|
|
850
|
+
exports.API_DOC_QUERY_KEY = API_DOC_QUERY_KEY;
|
|
851
|
+
exports.HydraRemoteDataSource = HydraRemoteDataSource;
|
|
852
|
+
exports.HydraResourceSchemaProvider = HydraResourceSchemaProvider;
|
|
853
|
+
exports.HydraResourceStoreProvider = HydraResourceStoreProvider;
|
|
854
|
+
exports.SchemaProvider = SchemaProvider;
|
|
855
|
+
exports.createHydraResourceStore = createHydraResourceStore;
|
|
856
|
+
exports.mapHydraSchemaToFields = mapHydraSchemaToFields;
|
|
857
|
+
exports.parseHydraDoc = parseHydraDoc;
|
|
858
|
+
exports.parseOpenApiDoc = parseOpenApiDoc;
|
|
859
|
+
exports.resolveRangeTag = resolveRangeTag;
|
|
860
|
+
exports.useHydraMetadata = useHydraMetadata;
|
|
861
|
+
exports.useResourceSchema = useResourceSchema;
|
|
862
|
+
exports.useSchemaContext = useSchemaContext;
|