@jskit-ai/crud-core 0.1.26 → 0.1.28

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.
@@ -0,0 +1,87 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { resolveCrudLookupFieldKeys } from "@jskit-ai/kernel/shared/support/crudLookup";
5
+
6
+ const listSearchQueryValidator = Object.freeze({
7
+ schema: Type.Object(
8
+ {
9
+ q: Type.Optional(Type.String({ minLength: 0 }))
10
+ },
11
+ { additionalProperties: false }
12
+ ),
13
+ normalize(payload = {}) {
14
+ const source = normalizeObjectInput(payload);
15
+ if (!Object.hasOwn(source, "q")) {
16
+ return {};
17
+ }
18
+
19
+ return {
20
+ q: normalizeText(source.q)
21
+ };
22
+ }
23
+ });
24
+
25
+ const lookupIncludeQueryValidator = Object.freeze({
26
+ schema: Type.Object(
27
+ {
28
+ include: Type.Optional(Type.String({ minLength: 0 }))
29
+ },
30
+ { additionalProperties: false }
31
+ ),
32
+ normalize(payload = {}) {
33
+ const source = normalizeObjectInput(payload);
34
+ if (!Object.hasOwn(source, "include")) {
35
+ return {};
36
+ }
37
+
38
+ return {
39
+ include: normalizeText(source.include)
40
+ };
41
+ }
42
+ });
43
+
44
+ function resolveCrudParentFilterKeys(resource = {}) {
45
+ const createSchemaProperties = resource?.operations?.create?.bodyValidator?.schema?.properties;
46
+ const allowedKeys = createSchemaProperties && typeof createSchemaProperties === "object" && !Array.isArray(createSchemaProperties)
47
+ ? Object.keys(createSchemaProperties)
48
+ : [];
49
+ return resolveCrudLookupFieldKeys(resource, {
50
+ allowKeys: allowedKeys
51
+ });
52
+ }
53
+
54
+ function createCrudParentFilterQueryValidator(resource = {}) {
55
+ const keys = resolveCrudParentFilterKeys(resource);
56
+ const schemaProperties = {};
57
+ for (const key of keys) {
58
+ schemaProperties[key] = Type.Optional(Type.String({ minLength: 1 }));
59
+ }
60
+
61
+ return Object.freeze({
62
+ schema: Type.Object(schemaProperties, { additionalProperties: false }),
63
+ normalize(payload = {}) {
64
+ const source = normalizeObjectInput(payload);
65
+ const normalized = {};
66
+ for (const key of keys) {
67
+ if (!Object.hasOwn(source, key)) {
68
+ continue;
69
+ }
70
+
71
+ const value = normalizeText(source[key]);
72
+ if (value) {
73
+ normalized[key] = value;
74
+ }
75
+ }
76
+
77
+ return normalized;
78
+ }
79
+ });
80
+ }
81
+
82
+ export {
83
+ listSearchQueryValidator,
84
+ lookupIncludeQueryValidator,
85
+ resolveCrudParentFilterKeys,
86
+ createCrudParentFilterQueryValidator
87
+ };
@@ -0,0 +1,546 @@
1
+ import { normalizeOpaqueId, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import {
3
+ normalizeCrudLookupNamespace,
4
+ resolveCrudLookupApiPathFromNamespace,
5
+ normalizeCrudLookupContainerKey,
6
+ resolveCrudLookupContainerKey
7
+ } from "@jskit-ai/kernel/shared/support/crudLookup";
8
+ import { normalizeCrudLookupApiPath } from "./lookupPathSupport.js";
9
+
10
+ const DEFAULT_LOOKUP_INCLUDE = "*";
11
+ const DEFAULT_LOOKUP_MAX_DEPTH = 3;
12
+ const MAX_LOOKUP_MAX_DEPTH = 10;
13
+
14
+ function normalizeLookupRelationEntry(entry = {}, outputKeys = new Set()) {
15
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
16
+ return null;
17
+ }
18
+
19
+ const key = normalizeText(entry.key);
20
+ if (!key || (outputKeys instanceof Set && !outputKeys.has(key))) {
21
+ return null;
22
+ }
23
+
24
+ const relation = entry.relation;
25
+ if (!relation || typeof relation !== "object" || Array.isArray(relation)) {
26
+ return null;
27
+ }
28
+
29
+ const relationKind = normalizeText(relation.kind).toLowerCase();
30
+ if (relationKind !== "lookup") {
31
+ return null;
32
+ }
33
+
34
+ const namespace =
35
+ normalizeCrudLookupNamespace(relation.namespace) ||
36
+ normalizeCrudLookupNamespace(relation.apiPath);
37
+ if (!namespace) {
38
+ return null;
39
+ }
40
+ const explicitApiPath = normalizeCrudLookupApiPath(relation.apiPath);
41
+ const apiPath = explicitApiPath || resolveCrudLookupApiPathFromNamespace(namespace);
42
+
43
+ const valueKey = normalizeText(relation.valueKey) || "id";
44
+
45
+ return {
46
+ key,
47
+ relation: {
48
+ kind: "lookup",
49
+ namespace,
50
+ apiPath,
51
+ valueKey,
52
+ hydrateOnList: relation.hydrateOnList !== false,
53
+ hydrateOnView: relation.hydrateOnView !== false
54
+ }
55
+ };
56
+ }
57
+
58
+ function normalizeLookupDefaultInclude(value) {
59
+ const normalized = normalizeText(value);
60
+ if (!normalized) {
61
+ return DEFAULT_LOOKUP_INCLUDE;
62
+ }
63
+
64
+ if (normalized.toLowerCase() === "none") {
65
+ return "none";
66
+ }
67
+
68
+ return normalized;
69
+ }
70
+
71
+ function normalizeLookupMaxDepth(value) {
72
+ const parsed = Number(value);
73
+ if (!Number.isInteger(parsed) || parsed < 0) {
74
+ return DEFAULT_LOOKUP_MAX_DEPTH;
75
+ }
76
+
77
+ return Math.min(parsed, MAX_LOOKUP_MAX_DEPTH);
78
+ }
79
+
80
+ function resolveLookupRuntimeDefaults(resource = {}) {
81
+ const lookupContract = resource?.contract?.lookup;
82
+ if (
83
+ lookupContract !== undefined &&
84
+ lookupContract !== null &&
85
+ (typeof lookupContract !== "object" || Array.isArray(lookupContract))
86
+ ) {
87
+ throw new TypeError("crud lookup runtime requires resource.contract.lookup to be an object when provided.");
88
+ }
89
+
90
+ return {
91
+ defaultInclude: normalizeLookupDefaultInclude(lookupContract?.defaultInclude),
92
+ maxDepth: normalizeLookupMaxDepth(lookupContract?.maxDepth)
93
+ };
94
+ }
95
+
96
+ function createCrudLookupRuntime(resource = {}, { outputKeys = [] } = {}) {
97
+ const outputKeySet = new Set(
98
+ (Array.isArray(outputKeys) ? outputKeys : [])
99
+ .map((key) => normalizeText(key))
100
+ .filter(Boolean)
101
+ );
102
+
103
+ const sourceEntries = Array.isArray(resource?.fieldMeta) ? resource.fieldMeta : [];
104
+ const lookupEntries = [];
105
+ const seenKeys = new Set();
106
+
107
+ for (const entry of sourceEntries) {
108
+ const normalizedEntry = normalizeLookupRelationEntry(entry, outputKeySet);
109
+ if (!normalizedEntry) {
110
+ continue;
111
+ }
112
+
113
+ if (seenKeys.has(normalizedEntry.key)) {
114
+ continue;
115
+ }
116
+ seenKeys.add(normalizedEntry.key);
117
+ lookupEntries.push(normalizedEntry);
118
+ }
119
+
120
+ const lookupEntryByKey = lookupEntries.reduce((accumulator, entry) => {
121
+ accumulator[entry.key] = entry;
122
+ return accumulator;
123
+ }, {});
124
+
125
+ const containerKey = resolveCrudLookupContainerKey(resource, {
126
+ context: "crud lookup runtime container key"
127
+ });
128
+ const defaults = resolveLookupRuntimeDefaults(resource);
129
+
130
+ return {
131
+ containerKey,
132
+ defaultInclude: defaults.defaultInclude,
133
+ maxDepth: defaults.maxDepth,
134
+ entries: lookupEntries,
135
+ byKey: lookupEntryByKey
136
+ };
137
+ }
138
+
139
+ function normalizeLookupIdentifier(value) {
140
+ const normalized = normalizeOpaqueId(value);
141
+ if (normalized == null) {
142
+ return "";
143
+ }
144
+
145
+ return normalizeText(normalized);
146
+ }
147
+
148
+ function normalizeIncludePaths(include, { defaultInclude = DEFAULT_LOOKUP_INCLUDE } = {}) {
149
+ const sourceInclude = normalizeText(include);
150
+ const normalizedInclude = sourceInclude || normalizeLookupDefaultInclude(defaultInclude);
151
+ if (!normalizedInclude || normalizedInclude.toLowerCase() === "none") {
152
+ return [];
153
+ }
154
+
155
+ const tokens = normalizeUniqueTextList(normalizedInclude.split(","));
156
+ const paths = [];
157
+
158
+ for (const token of tokens) {
159
+ const normalizedToken = normalizeText(token);
160
+ if (!normalizedToken) {
161
+ continue;
162
+ }
163
+
164
+ if (normalizedToken.toLowerCase() === "none") {
165
+ return [];
166
+ }
167
+
168
+ const segments = normalizedToken
169
+ .split(".")
170
+ .map((entry) => normalizeText(entry))
171
+ .filter(Boolean);
172
+
173
+ if (segments.length > 0) {
174
+ paths.push(segments);
175
+ }
176
+ }
177
+
178
+ return paths;
179
+ }
180
+
181
+ function resolveChildIncludeFromPaths(paths = []) {
182
+ const entries = Array.isArray(paths) ? paths : [];
183
+ const pathKeys = new Set();
184
+ let includesAll = false;
185
+
186
+ for (const path of entries) {
187
+ if (!Array.isArray(path) || path.length < 1) {
188
+ continue;
189
+ }
190
+
191
+ const normalizedPath = path
192
+ .map((entry) => normalizeText(entry))
193
+ .filter(Boolean);
194
+ if (normalizedPath.length < 1) {
195
+ continue;
196
+ }
197
+ if (normalizedPath[0] === "*") {
198
+ includesAll = true;
199
+ continue;
200
+ }
201
+ pathKeys.add(normalizedPath.join("."));
202
+ }
203
+
204
+ if (includesAll) {
205
+ return "*";
206
+ }
207
+ if (pathKeys.size < 1) {
208
+ return "none";
209
+ }
210
+ return [...pathKeys].join(",");
211
+ }
212
+
213
+ function buildLookupHydrationPlan(
214
+ runtime = {},
215
+ include,
216
+ {
217
+ mode = "list",
218
+ context = "crudRepository",
219
+ includeWasExplicit = false
220
+ } = {}
221
+ ) {
222
+ const entries = Array.isArray(runtime?.entries) ? runtime.entries : [];
223
+ if (entries.length < 1) {
224
+ return {
225
+ entries: [],
226
+ childIncludeByKey: {}
227
+ };
228
+ }
229
+
230
+ const includePaths = normalizeIncludePaths(include, {
231
+ defaultInclude: runtime?.defaultInclude
232
+ });
233
+ if (includePaths.length < 1) {
234
+ return {
235
+ entries: [],
236
+ childIncludeByKey: {}
237
+ };
238
+ }
239
+
240
+ const shouldHydrateByMode = mode === "view"
241
+ ? (entry) => entry?.relation?.hydrateOnView !== false
242
+ : (entry) => entry?.relation?.hydrateOnList !== false;
243
+
244
+ const selectedByKey = new Map();
245
+ function ensureSelection(entry = null) {
246
+ if (!entry) {
247
+ return null;
248
+ }
249
+
250
+ if (!includeWasExplicit && !shouldHydrateByMode(entry)) {
251
+ return null;
252
+ }
253
+
254
+ if (!selectedByKey.has(entry.key)) {
255
+ selectedByKey.set(entry.key, {
256
+ entry,
257
+ childPaths: [],
258
+ childPathSet: new Set()
259
+ });
260
+ }
261
+
262
+ return selectedByKey.get(entry.key);
263
+ }
264
+
265
+ function appendChildPath(selection, segments = []) {
266
+ const sourceSegments = Array.isArray(segments) ? segments : [];
267
+ if (sourceSegments.length < 1) {
268
+ return;
269
+ }
270
+
271
+ const normalizedSegments = sourceSegments
272
+ .map((entry) => normalizeText(entry))
273
+ .filter(Boolean);
274
+ if (normalizedSegments.length < 1) {
275
+ return;
276
+ }
277
+
278
+ const pathKey = normalizedSegments.join(".");
279
+ if (selection.childPathSet.has(pathKey)) {
280
+ return;
281
+ }
282
+
283
+ selection.childPathSet.add(pathKey);
284
+ selection.childPaths.push(normalizedSegments);
285
+ }
286
+
287
+ for (const pathSegments of includePaths) {
288
+ const [head, ...tail] = pathSegments;
289
+ if (!head) {
290
+ continue;
291
+ }
292
+
293
+ if (head === "*") {
294
+ const wildcardTail = tail.length > 0 ? tail : ["*"];
295
+ for (const entry of entries) {
296
+ const selection = ensureSelection(entry);
297
+ if (!selection) {
298
+ continue;
299
+ }
300
+ appendChildPath(selection, wildcardTail);
301
+ }
302
+ continue;
303
+ }
304
+
305
+ const entry = runtime?.byKey?.[head] || null;
306
+ if (!entry) {
307
+ throw new Error(`${context} include references unknown lookup key "${head}".`);
308
+ }
309
+
310
+ const selection = ensureSelection(entry);
311
+ if (!selection) {
312
+ continue;
313
+ }
314
+
315
+ if (tail.length > 0) {
316
+ appendChildPath(selection, tail);
317
+ }
318
+ }
319
+
320
+ const selectedEntries = [];
321
+ const childIncludeByKey = {};
322
+ for (const selection of selectedByKey.values()) {
323
+ selectedEntries.push(selection.entry);
324
+ childIncludeByKey[selection.entry.key] = resolveChildIncludeFromPaths(selection.childPaths);
325
+ }
326
+
327
+ return {
328
+ entries: selectedEntries,
329
+ childIncludeByKey
330
+ };
331
+ }
332
+
333
+ function resolveLookupProviderResolver(repositoryOptions = {}, callOptions = {}, { context = "crudRepository" } = {}) {
334
+ const resolver = callOptions?.resolveLookupProvider || repositoryOptions?.resolveLookupProvider;
335
+ if (typeof resolver !== "function") {
336
+ throw new TypeError(`${context} requires resolveLookupProvider(relation) to hydrate lookups.`);
337
+ }
338
+ return resolver;
339
+ }
340
+
341
+ function resolveLookupDepthRuntime(runtime = {}, repositoryOptions = {}, callOptions = {}) {
342
+ const maxDepth = normalizeLookupMaxDepth(
343
+ callOptions?.lookupMaxDepth ?? repositoryOptions?.lookupMaxDepth ?? runtime?.maxDepth
344
+ );
345
+
346
+ const parsedDepth = Number(callOptions?.lookupDepth);
347
+ const depth = Number.isInteger(parsedDepth) && parsedDepth >= 0 ? parsedDepth : 0;
348
+
349
+ return {
350
+ depth,
351
+ maxDepth
352
+ };
353
+ }
354
+
355
+ function buildLookupGroupKey(relation = {}) {
356
+ return `${relation.namespace}::${relation.valueKey}`;
357
+ }
358
+
359
+ function normalizeLookupRelationValues(records = [], entries = [], childIncludeByKey = {}) {
360
+ const byGroup = new Map();
361
+ for (const entry of entries) {
362
+ const relation = entry.relation;
363
+ const groupKey = buildLookupGroupKey(relation);
364
+ if (!byGroup.has(groupKey)) {
365
+ byGroup.set(groupKey, {
366
+ relation,
367
+ entries: [],
368
+ values: [],
369
+ childIncludes: new Set()
370
+ });
371
+ }
372
+
373
+ const group = byGroup.get(groupKey);
374
+ group.entries.push(entry);
375
+ group.childIncludes.add(normalizeText(childIncludeByKey?.[entry.key]) || "none");
376
+ }
377
+
378
+ for (const group of byGroup.values()) {
379
+ const seen = new Set();
380
+ for (const record of records) {
381
+ for (const entry of group.entries) {
382
+ const rawValue = record?.[entry.key];
383
+ const normalized = normalizeLookupIdentifier(rawValue);
384
+ if (!normalized || seen.has(normalized)) {
385
+ continue;
386
+ }
387
+ seen.add(normalized);
388
+ group.values.push(rawValue);
389
+ }
390
+ }
391
+ }
392
+
393
+ return byGroup;
394
+ }
395
+
396
+ function resolveGroupChildInclude(group = {}) {
397
+ const sourceIncludes = group?.childIncludes instanceof Set ? [...group.childIncludes] : [];
398
+ const includeSet = new Set();
399
+ let includeAll = false;
400
+
401
+ for (const includeValue of sourceIncludes) {
402
+ const normalized = normalizeText(includeValue);
403
+ if (!normalized || normalized.toLowerCase() === "none") {
404
+ continue;
405
+ }
406
+
407
+ if (normalized === "*") {
408
+ includeAll = true;
409
+ continue;
410
+ }
411
+
412
+ for (const token of normalizeUniqueTextList(normalized.split(","))) {
413
+ if (token === "*") {
414
+ includeAll = true;
415
+ continue;
416
+ }
417
+ includeSet.add(token);
418
+ }
419
+ }
420
+
421
+ if (includeAll) {
422
+ return "*";
423
+ }
424
+ if (includeSet.size < 1) {
425
+ return "none";
426
+ }
427
+ return [...includeSet].join(",");
428
+ }
429
+
430
+ function normalizeLookupProvider(provider, relation = {}, { context = "crudRepository" } = {}) {
431
+ if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
432
+ throw new Error(`${context} could not resolve lookup provider for namespace "${relation.namespace}".`);
433
+ }
434
+ if (typeof provider.listByIds !== "function") {
435
+ throw new Error(`${context} lookup provider for namespace "${relation.namespace}" must expose listByIds(ids, options).`);
436
+ }
437
+ return provider;
438
+ }
439
+
440
+ function buildLookupRecordMap(records = [], valueKey = "") {
441
+ const lookupMap = new Map();
442
+ for (const record of Array.isArray(records) ? records : []) {
443
+ const lookupId = normalizeLookupIdentifier(record?.[valueKey]);
444
+ if (!lookupId || lookupMap.has(lookupId)) {
445
+ continue;
446
+ }
447
+ lookupMap.set(lookupId, record);
448
+ }
449
+ return lookupMap;
450
+ }
451
+
452
+ async function hydrateCrudLookupRecords(
453
+ records = [],
454
+ runtime = {},
455
+ {
456
+ include,
457
+ mode = "list",
458
+ repositoryOptions = {},
459
+ callOptions = {}
460
+ } = {}
461
+ ) {
462
+ const sourceRecords = Array.isArray(records) ? records : [];
463
+ if (sourceRecords.length < 1) {
464
+ return sourceRecords;
465
+ }
466
+
467
+ const depthRuntime = resolveLookupDepthRuntime(runtime, repositoryOptions, callOptions);
468
+ if (depthRuntime.depth >= depthRuntime.maxDepth) {
469
+ return sourceRecords;
470
+ }
471
+
472
+ const lookupContainerKey = normalizeCrudLookupContainerKey(runtime?.containerKey, {
473
+ context: `${runtime?.context || "crudRepository"} lookup runtime container key`
474
+ });
475
+
476
+ const lookupPlan = buildLookupHydrationPlan(runtime, include, {
477
+ mode,
478
+ context: runtime?.context || "crudRepository",
479
+ includeWasExplicit: normalizeText(include).length > 0
480
+ });
481
+ const selectedEntries = lookupPlan.entries;
482
+ if (selectedEntries.length < 1) {
483
+ return sourceRecords;
484
+ }
485
+
486
+ const resolveLookupProvider = resolveLookupProviderResolver(repositoryOptions, callOptions, {
487
+ context: runtime?.context || "crudRepository"
488
+ });
489
+
490
+ const relationGroups = normalizeLookupRelationValues(sourceRecords, selectedEntries, lookupPlan.childIncludeByKey);
491
+ const groupRecordMaps = new Map();
492
+ for (const group of relationGroups.values()) {
493
+ if (group.values.length < 1) {
494
+ groupRecordMaps.set(buildLookupGroupKey(group.relation), new Map());
495
+ continue;
496
+ }
497
+
498
+ const provider = normalizeLookupProvider(resolveLookupProvider(group.relation), group.relation, {
499
+ context: runtime?.context || "crudRepository"
500
+ });
501
+ const childInclude = resolveGroupChildInclude(group);
502
+ const groupRecords = await provider.listByIds(group.values, {
503
+ ...callOptions,
504
+ include: childInclude,
505
+ lookupDepth: depthRuntime.depth + 1,
506
+ lookupMaxDepth: depthRuntime.maxDepth,
507
+ valueKey: group.relation.valueKey
508
+ });
509
+ groupRecordMaps.set(
510
+ buildLookupGroupKey(group.relation),
511
+ buildLookupRecordMap(groupRecords, group.relation.valueKey)
512
+ );
513
+ }
514
+
515
+ return sourceRecords.map((record) => {
516
+ const baseRecord = record && typeof record === "object" && !Array.isArray(record) ? record : {};
517
+ const existingLookups =
518
+ baseRecord[lookupContainerKey] &&
519
+ typeof baseRecord[lookupContainerKey] === "object" &&
520
+ !Array.isArray(baseRecord[lookupContainerKey])
521
+ ? baseRecord[lookupContainerKey]
522
+ : {};
523
+ const nextLookups = { ...existingLookups };
524
+
525
+ for (const entry of selectedEntries) {
526
+ const relation = entry.relation;
527
+ const lookupMap = groupRecordMaps.get(buildLookupGroupKey(relation)) || new Map();
528
+ const lookupId = normalizeLookupIdentifier(baseRecord?.[entry.key]);
529
+ if (!lookupId) {
530
+ nextLookups[entry.key] = null;
531
+ continue;
532
+ }
533
+ nextLookups[entry.key] = lookupMap.get(lookupId) || null;
534
+ }
535
+
536
+ return {
537
+ ...baseRecord,
538
+ [lookupContainerKey]: nextLookups
539
+ };
540
+ });
541
+ }
542
+
543
+ export {
544
+ createCrudLookupRuntime,
545
+ hydrateCrudLookupRecords
546
+ };
@@ -0,0 +1,45 @@
1
+ import {
2
+ normalizeCrudLookupApiPath,
3
+ normalizeCrudLookupNamespace
4
+ } from "@jskit-ai/kernel/shared/support/crudLookup";
5
+ import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
6
+
7
+ function requireCrudLookupNamespace(value = "", { context = "crudLookupProvider" } = {}) {
8
+ const normalizedNamespace = normalizeCrudLookupNamespace(value);
9
+ if (!normalizedNamespace) {
10
+ throw new Error(`${context} requires relation.namespace.`);
11
+ }
12
+
13
+ return normalizedNamespace;
14
+ }
15
+
16
+ function resolveCrudLookupProviderToken(namespace = "", { context = "crudLookupProvider" } = {}) {
17
+ const normalizedNamespace = requireCrudLookupNamespace(namespace, {
18
+ context
19
+ });
20
+ const tokenPart = normalizedNamespace
21
+ .split("/")
22
+ .map((segment) => toSnakeCase(segment))
23
+ .filter(Boolean)
24
+ .join(".");
25
+ return `crud.lookup.${tokenPart}`;
26
+ }
27
+
28
+ function resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookupProvider" } = {}) {
29
+ const normalizedNamespace =
30
+ normalizeCrudLookupNamespace(relation?.namespace) ||
31
+ normalizeCrudLookupNamespace(relation?.apiPath);
32
+ if (!normalizedNamespace) {
33
+ throw new Error(`${context} requires relation.namespace.`);
34
+ }
35
+
36
+ return normalizedNamespace;
37
+ }
38
+
39
+ export {
40
+ normalizeCrudLookupApiPath,
41
+ normalizeCrudLookupNamespace,
42
+ requireCrudLookupNamespace,
43
+ resolveCrudLookupNamespaceFromRelation,
44
+ resolveCrudLookupProviderToken
45
+ };
@@ -0,0 +1,43 @@
1
+ import {
2
+ resolveCrudLookupProviderToken,
3
+ resolveCrudLookupNamespaceFromRelation
4
+ } from "./lookupPathSupport.js";
5
+
6
+ function createCrudLookupProviderResolver(scope, { context = "crudLookupProvider" } = {}) {
7
+ if (!scope || typeof scope.make !== "function") {
8
+ throw new Error(`${context} requires scope.make().`);
9
+ }
10
+
11
+ return function resolveLookupProvider(relation = {}) {
12
+ const namespace = resolveCrudLookupNamespaceFromRelation(relation, {
13
+ context
14
+ });
15
+ return scope.make(
16
+ resolveCrudLookupProviderToken(namespace, {
17
+ context
18
+ })
19
+ );
20
+ };
21
+ }
22
+
23
+ function createCrudLookupProvider(repository, { context = "crudLookupProvider" } = {}) {
24
+ if (!repository || typeof repository.listByIds !== "function") {
25
+ throw new Error(`${context} requires repository.listByIds(ids, options).`);
26
+ }
27
+
28
+ return Object.freeze({
29
+ async listByIds(ids = [], options = {}) {
30
+ const include = options?.include === undefined ? "none" : options.include;
31
+ return repository.listByIds(ids, {
32
+ ...options,
33
+ include
34
+ });
35
+ }
36
+ });
37
+ }
38
+
39
+ export {
40
+ resolveCrudLookupProviderToken,
41
+ createCrudLookupProviderResolver,
42
+ createCrudLookupProvider
43
+ };