@jskit-ai/crud-core 0.1.30 → 0.1.31
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/package.descriptor.mjs +2 -2
- package/package.json +7 -6
- package/src/server/createCrudRepositoryFromResource.js +18 -12
- package/src/server/createHooksToCollectChildren.js +247 -0
- package/src/server/lookupHydration.js +172 -11
- package/src/server/lookupProviders.js +33 -1
- package/src/server/repositoryMethods.js +808 -58
- package/test/createCrudRepositoryFromResource.test.js +1091 -71
- package/test/createHooksToCollectChildren.test.js +251 -0
- package/test/lookupProviders.test.js +32 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/crud-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.31",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
7
7
|
dependsOn: [
|
|
@@ -26,7 +26,7 @@ export default Object.freeze({
|
|
|
26
26
|
mutations: {
|
|
27
27
|
dependencies: {
|
|
28
28
|
runtime: {
|
|
29
|
-
"@jskit-ai/crud-core": "0.1.
|
|
29
|
+
"@jskit-ai/crud-core": "0.1.31"
|
|
30
30
|
},
|
|
31
31
|
dev: {}
|
|
32
32
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./shared/crudNamespaceSupport": "./src/shared/crudNamespaceSupport.js",
|
|
15
15
|
"./server/repositorySupport": "./src/server/repositorySupport.js",
|
|
16
16
|
"./server/repositoryMethods": "./src/server/repositoryMethods.js",
|
|
17
|
+
"./server/createHooksToCollectChildren": "./src/server/createHooksToCollectChildren.js",
|
|
17
18
|
"./server/createCrudRepositoryFromResource": "./src/server/createCrudRepositoryFromResource.js",
|
|
18
19
|
"./server/lookupProviders": "./src/server/lookupProviders.js",
|
|
19
20
|
"./server/serviceEvents": "./src/server/serviceEvents.js",
|
|
@@ -24,11 +25,11 @@
|
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@tanstack/vue-query": "^5.90.5",
|
|
27
|
-
"@jskit-ai/kernel": "0.1.
|
|
28
|
-
"@jskit-ai/realtime": "0.1.
|
|
29
|
-
"@jskit-ai/shell-web": "0.1.
|
|
30
|
-
"@jskit-ai/users-core": "0.1.
|
|
31
|
-
"@jskit-ai/users-web": "0.1.
|
|
28
|
+
"@jskit-ai/kernel": "0.1.23",
|
|
29
|
+
"@jskit-ai/realtime": "0.1.22",
|
|
30
|
+
"@jskit-ai/shell-web": "0.1.22",
|
|
31
|
+
"@jskit-ai/users-core": "0.1.32",
|
|
32
|
+
"@jskit-ai/users-web": "0.1.37",
|
|
32
33
|
"typebox": "^1.0.81"
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
crudRepositoryList,
|
|
4
4
|
crudRepositoryFindById,
|
|
5
5
|
crudRepositoryListByIds,
|
|
6
|
+
crudRepositoryListByForeignIds,
|
|
6
7
|
crudRepositoryCreate,
|
|
7
8
|
crudRepositoryUpdateById,
|
|
8
9
|
crudRepositoryDeleteById
|
|
@@ -19,34 +20,39 @@ function createCrudRepositoryFromResource(resource = {}, { context = "crudReposi
|
|
|
19
20
|
throw new TypeError("crudRepository requires knex.");
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
async function listRecords(query = {}, callOptions = {}) {
|
|
23
|
-
return crudRepositoryList(runtime, knex, query, options, callOptions);
|
|
23
|
+
async function listRecords(query = {}, callOptions = {}, hooks = null) {
|
|
24
|
+
return crudRepositoryList(runtime, knex, query, options, callOptions, hooks);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
async function findById(recordId, callOptions = {}) {
|
|
27
|
-
return crudRepositoryFindById(runtime, knex, recordId, options, callOptions);
|
|
27
|
+
async function findById(recordId, callOptions = {}, hooks = null) {
|
|
28
|
+
return crudRepositoryFindById(runtime, knex, recordId, options, callOptions, hooks);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async function listByIds(ids = [], callOptions = {}) {
|
|
31
|
-
return crudRepositoryListByIds(runtime, knex, ids, options, callOptions);
|
|
31
|
+
async function listByIds(ids = [], callOptions = {}, hooks = null) {
|
|
32
|
+
return crudRepositoryListByIds(runtime, knex, ids, options, callOptions, hooks);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
async function
|
|
35
|
-
return
|
|
35
|
+
async function listByForeignIds(ids = [], foreignKey = "", callOptions = {}, hooks = null) {
|
|
36
|
+
return crudRepositoryListByForeignIds(runtime, knex, ids, foreignKey, options, callOptions, hooks);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
async function
|
|
39
|
-
return
|
|
39
|
+
async function create(payload = {}, callOptions = {}, hooks = null) {
|
|
40
|
+
return crudRepositoryCreate(runtime, knex, payload, options, callOptions, hooks);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
async function
|
|
43
|
-
return
|
|
43
|
+
async function updateById(recordId, patch = {}, callOptions = {}, hooks = null) {
|
|
44
|
+
return crudRepositoryUpdateById(runtime, knex, recordId, patch, options, callOptions, hooks);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function deleteById(recordId, callOptions = {}, hooks = null) {
|
|
48
|
+
return crudRepositoryDeleteById(runtime, knex, recordId, options, callOptions, hooks);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
return Object.freeze({
|
|
47
52
|
list: listRecords,
|
|
48
53
|
findById,
|
|
49
54
|
listByIds,
|
|
55
|
+
listByForeignIds,
|
|
50
56
|
create,
|
|
51
57
|
updateById,
|
|
52
58
|
deleteById
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeOwnerKey(value) {
|
|
4
|
+
if (value === null || value === undefined) {
|
|
5
|
+
return "";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "number") {
|
|
12
|
+
return Number.isFinite(value) ? String(value) : "";
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "bigint") {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return String(value).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toDefaultChildCallOptions(callOptions = {}) {
|
|
22
|
+
const nextOptions = {};
|
|
23
|
+
if (Object.hasOwn(callOptions, "trx")) {
|
|
24
|
+
nextOptions.trx = callOptions.trx;
|
|
25
|
+
}
|
|
26
|
+
if (Object.hasOwn(callOptions, "visibilityContext")) {
|
|
27
|
+
nextOptions.visibilityContext = callOptions.visibilityContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return nextOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveListChildrenHandler(options = {}, { context = "createHooksToCollectChildren" } = {}) {
|
|
34
|
+
if (typeof options.listChildren === "function") {
|
|
35
|
+
return options.listChildren;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const childRepository = options.childRepository;
|
|
39
|
+
const childListMethod = normalizeText(options.childListMethod) || "listByIds";
|
|
40
|
+
const childForeignKey = normalizeText(options.childForeignKey);
|
|
41
|
+
if (!childRepository || typeof childRepository !== "object" || Array.isArray(childRepository)) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
`${context} requires listChildren(ids, options, ctx) or childRepository.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const listChildren = childRepository[childListMethod];
|
|
48
|
+
if (typeof listChildren !== "function") {
|
|
49
|
+
throw new TypeError(`${context} requires childRepository.${childListMethod} to be a function.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (childListMethod === "listByIds") {
|
|
53
|
+
if (!childForeignKey) {
|
|
54
|
+
throw new TypeError(`${context} requires childForeignKey when using childRepository.listByIds.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (ids = [], childCallOptions = {}, hookContext = {}) =>
|
|
58
|
+
listChildren.call(childRepository, ids, {
|
|
59
|
+
...childCallOptions,
|
|
60
|
+
valueKey: childForeignKey
|
|
61
|
+
}, hookContext);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (childListMethod === "listByForeignIds") {
|
|
65
|
+
if (!childForeignKey) {
|
|
66
|
+
throw new TypeError(`${context} requires childForeignKey when using childRepository.listByForeignIds.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (ids = [], childCallOptions = {}, hookContext = {}) =>
|
|
70
|
+
listChildren.call(childRepository, ids, childForeignKey, childCallOptions, hookContext);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (ids = [], childCallOptions = {}, hookContext = {}) => {
|
|
74
|
+
if (childForeignKey) {
|
|
75
|
+
return listChildren.call(childRepository, ids, childForeignKey, childCallOptions, hookContext);
|
|
76
|
+
}
|
|
77
|
+
return listChildren.call(childRepository, ids, childCallOptions, hookContext);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveGetChildOwnerId(options = {}, { context = "createHooksToCollectChildren" } = {}) {
|
|
82
|
+
if (typeof options.getChildOwnerId === "function") {
|
|
83
|
+
return options.getChildOwnerId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const childOwnerIdKey = normalizeText(options.childOwnerIdKey);
|
|
87
|
+
const childForeignKey = normalizeText(options.childForeignKey);
|
|
88
|
+
const ownerKey = childOwnerIdKey || childForeignKey;
|
|
89
|
+
if (!ownerKey) {
|
|
90
|
+
throw new TypeError(`${context} requires childOwnerIdKey, childForeignKey, or getChildOwnerId.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (child = {}) => {
|
|
94
|
+
if (!child || typeof child !== "object" || Array.isArray(child)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (childOwnerIdKey && Object.hasOwn(child, childOwnerIdKey)) {
|
|
98
|
+
return child[childOwnerIdKey];
|
|
99
|
+
}
|
|
100
|
+
return child[ownerKey];
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveLookupContainerKey(record = {}, hookContext = {}, options = {}) {
|
|
105
|
+
const explicitContainerKey = normalizeText(options.lookupContainerKey);
|
|
106
|
+
if (explicitContainerKey) {
|
|
107
|
+
return explicitContainerKey;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const runtimeContainerKey = normalizeText(hookContext?.runtime?.lookup?.containerKey);
|
|
111
|
+
if (runtimeContainerKey) {
|
|
112
|
+
return runtimeContainerKey;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Object.hasOwn(record, "lookups")) {
|
|
116
|
+
return "lookups";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return "lookups";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createHooksToCollectChildren(options = {}) {
|
|
123
|
+
const context = normalizeText(options.context) || "createHooksToCollectChildren";
|
|
124
|
+
const childKey = normalizeText(options.childKey);
|
|
125
|
+
if (!childKey) {
|
|
126
|
+
throw new TypeError(`${context} requires childKey.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const listChildren = resolveListChildrenHandler(options, {
|
|
130
|
+
context
|
|
131
|
+
});
|
|
132
|
+
const getParentId = typeof options.getParentId === "function"
|
|
133
|
+
? options.getParentId
|
|
134
|
+
: (record = {}) => record?.id;
|
|
135
|
+
const getChildOwnerId = resolveGetChildOwnerId(options, {
|
|
136
|
+
context
|
|
137
|
+
});
|
|
138
|
+
const normalizeCollectionOwnerKey = typeof options.normalizeOwnerKey === "function"
|
|
139
|
+
? options.normalizeOwnerKey
|
|
140
|
+
: normalizeOwnerKey;
|
|
141
|
+
const buildChildCallOptions = typeof options.buildChildCallOptions === "function"
|
|
142
|
+
? options.buildChildCallOptions
|
|
143
|
+
: ({ callOptions = {} } = {}) => toDefaultChildCallOptions(callOptions);
|
|
144
|
+
const stateMapKey = options.stateMapKey || Symbol(`crud.children.${childKey}`);
|
|
145
|
+
const attachToLookupContainer = options.attachToLookupContainer !== false;
|
|
146
|
+
const attachChildren = typeof options.attachChildren === "function"
|
|
147
|
+
? options.attachChildren
|
|
148
|
+
: (record = {}, children = [], hookContext = {}) => {
|
|
149
|
+
if (!attachToLookupContainer) {
|
|
150
|
+
return {
|
|
151
|
+
...record,
|
|
152
|
+
[childKey]: children
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const containerKey = resolveLookupContainerKey(record, hookContext, options);
|
|
157
|
+
const sourceContainer = record?.[containerKey];
|
|
158
|
+
const normalizedContainer =
|
|
159
|
+
sourceContainer && typeof sourceContainer === "object" && !Array.isArray(sourceContainer)
|
|
160
|
+
? sourceContainer
|
|
161
|
+
: {};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...record,
|
|
165
|
+
[containerKey]: {
|
|
166
|
+
...normalizedContainer,
|
|
167
|
+
[childKey]: children
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
async afterQuery(records = [], ctx = {}) {
|
|
174
|
+
const normalizedRecords = Array.isArray(records) ? records : [];
|
|
175
|
+
const ownerIds = [];
|
|
176
|
+
const seenOwnerKeys = new Set();
|
|
177
|
+
|
|
178
|
+
for (const record of normalizedRecords) {
|
|
179
|
+
const ownerId = getParentId(record, ctx);
|
|
180
|
+
const ownerKey = normalizeCollectionOwnerKey(ownerId);
|
|
181
|
+
if (!ownerKey || seenOwnerKeys.has(ownerKey)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
seenOwnerKeys.add(ownerKey);
|
|
186
|
+
ownerIds.push(ownerId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const state = ctx?.state && typeof ctx.state === "object" ? ctx.state : null;
|
|
190
|
+
if (!state) {
|
|
191
|
+
throw new TypeError(`${context} requires ctx.state object.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ownerIds.length < 1) {
|
|
195
|
+
state[stateMapKey] = new Map();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const childCallOptions = buildChildCallOptions({
|
|
200
|
+
callOptions: ctx.callOptions || {},
|
|
201
|
+
records: normalizedRecords,
|
|
202
|
+
ownerIds,
|
|
203
|
+
context: ctx
|
|
204
|
+
});
|
|
205
|
+
const children = await listChildren(ownerIds, childCallOptions, ctx);
|
|
206
|
+
if (!Array.isArray(children)) {
|
|
207
|
+
throw new TypeError(`${context} listChildren must return an array.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const childrenByOwnerKey = new Map();
|
|
211
|
+
for (const child of children) {
|
|
212
|
+
const childOwnerId = getChildOwnerId(child, ctx);
|
|
213
|
+
const childOwnerKey = normalizeCollectionOwnerKey(childOwnerId);
|
|
214
|
+
if (!childOwnerKey) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const currentList = childrenByOwnerKey.get(childOwnerKey);
|
|
219
|
+
if (currentList) {
|
|
220
|
+
currentList.push(child);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
childrenByOwnerKey.set(childOwnerKey, [child]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
state[stateMapKey] = childrenByOwnerKey;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
transformReturnedRecord(record = {}, ctx = {}) {
|
|
231
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
232
|
+
return record;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const state = ctx?.state && typeof ctx.state === "object" ? ctx.state : null;
|
|
236
|
+
const childrenByOwnerKey = state ? state[stateMapKey] : null;
|
|
237
|
+
const ownerKey = normalizeCollectionOwnerKey(getParentId(record, ctx));
|
|
238
|
+
const children = ownerKey && childrenByOwnerKey instanceof Map
|
|
239
|
+
? (childrenByOwnerKey.get(ownerKey) || [])
|
|
240
|
+
: [];
|
|
241
|
+
|
|
242
|
+
return attachChildren(record, children, ctx);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { createHooksToCollectChildren };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeOpaqueId, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeVisibilityContext } from "@jskit-ai/kernel/shared/support/visibility";
|
|
2
3
|
import {
|
|
3
4
|
normalizeCrudLookupNamespace,
|
|
4
5
|
resolveCrudLookupApiPathFromNamespace,
|
|
@@ -10,6 +11,13 @@ import { normalizeCrudLookupApiPath } from "./lookupPathSupport.js";
|
|
|
10
11
|
const DEFAULT_LOOKUP_INCLUDE = "*";
|
|
11
12
|
const DEFAULT_LOOKUP_MAX_DEPTH = 3;
|
|
12
13
|
const MAX_LOOKUP_MAX_DEPTH = 10;
|
|
14
|
+
const LOOKUP_PROVIDER_OWNERSHIP_FILTER_VALUES = Object.freeze([
|
|
15
|
+
"public",
|
|
16
|
+
"user",
|
|
17
|
+
"workspace",
|
|
18
|
+
"workspace_user"
|
|
19
|
+
]);
|
|
20
|
+
const LOOKUP_PROVIDER_OWNERSHIP_FILTER_SET = new Set(LOOKUP_PROVIDER_OWNERSHIP_FILTER_VALUES);
|
|
13
21
|
|
|
14
22
|
function normalizeLookupRelationEntry(entry = {}, outputKeys = new Set()) {
|
|
15
23
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
@@ -17,7 +25,7 @@ function normalizeLookupRelationEntry(entry = {}, outputKeys = new Set()) {
|
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const key = normalizeText(entry.key);
|
|
20
|
-
if (!key
|
|
28
|
+
if (!key) {
|
|
21
29
|
return null;
|
|
22
30
|
}
|
|
23
31
|
|
|
@@ -27,7 +35,7 @@ function normalizeLookupRelationEntry(entry = {}, outputKeys = new Set()) {
|
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
const relationKind = normalizeText(relation.kind).toLowerCase();
|
|
30
|
-
if (relationKind !== "lookup") {
|
|
38
|
+
if (relationKind !== "lookup" && relationKind !== "collection") {
|
|
31
39
|
return null;
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -40,15 +48,43 @@ function normalizeLookupRelationEntry(entry = {}, outputKeys = new Set()) {
|
|
|
40
48
|
const explicitApiPath = normalizeCrudLookupApiPath(relation.apiPath);
|
|
41
49
|
const apiPath = explicitApiPath || resolveCrudLookupApiPathFromNamespace(namespace);
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
if (relationKind === "lookup") {
|
|
52
|
+
if (outputKeys instanceof Set && !outputKeys.has(key)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const valueKey = normalizeText(relation.valueKey) || "id";
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
key,
|
|
59
|
+
relation: {
|
|
60
|
+
kind: "lookup",
|
|
61
|
+
namespace,
|
|
62
|
+
apiPath,
|
|
63
|
+
valueKey,
|
|
64
|
+
hydrateOnList: relation.hydrateOnList !== false,
|
|
65
|
+
hydrateOnView: relation.hydrateOnView !== false
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const foreignKey = normalizeText(relation.foreignKey);
|
|
71
|
+
if (!foreignKey) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parentValueKey = normalizeText(relation.parentValueKey) || "id";
|
|
76
|
+
if (outputKeys instanceof Set && !outputKeys.has(parentValueKey)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
44
79
|
|
|
45
80
|
return {
|
|
46
81
|
key,
|
|
47
82
|
relation: {
|
|
48
|
-
kind: "
|
|
83
|
+
kind: "collection",
|
|
49
84
|
namespace,
|
|
50
85
|
apiPath,
|
|
51
|
-
|
|
86
|
+
foreignKey,
|
|
87
|
+
parentValueKey,
|
|
52
88
|
hydrateOnList: relation.hydrateOnList !== false,
|
|
53
89
|
hydrateOnView: relation.hydrateOnView !== false
|
|
54
90
|
}
|
|
@@ -125,9 +161,13 @@ function createCrudLookupRuntime(resource = {}, { outputKeys = [] } = {}) {
|
|
|
125
161
|
const containerKey = resolveCrudLookupContainerKey(resource, {
|
|
126
162
|
context: "crud lookup runtime container key"
|
|
127
163
|
});
|
|
164
|
+
const namespace =
|
|
165
|
+
normalizeCrudLookupNamespace(resource?.resource) ||
|
|
166
|
+
normalizeCrudLookupNamespace(resource?.apiPath);
|
|
128
167
|
const defaults = resolveLookupRuntimeDefaults(resource);
|
|
129
168
|
|
|
130
169
|
return {
|
|
170
|
+
namespace,
|
|
131
171
|
containerKey,
|
|
132
172
|
defaultInclude: defaults.defaultInclude,
|
|
133
173
|
maxDepth: defaults.maxDepth,
|
|
@@ -216,7 +256,8 @@ function buildLookupHydrationPlan(
|
|
|
216
256
|
{
|
|
217
257
|
mode = "list",
|
|
218
258
|
context = "crudRepository",
|
|
219
|
-
includeWasExplicit = false
|
|
259
|
+
includeWasExplicit = false,
|
|
260
|
+
skippedNamespaces = new Set()
|
|
220
261
|
} = {}
|
|
221
262
|
) {
|
|
222
263
|
const entries = Array.isArray(runtime?.entries) ? runtime.entries : [];
|
|
@@ -246,6 +287,9 @@ function buildLookupHydrationPlan(
|
|
|
246
287
|
if (!entry) {
|
|
247
288
|
return null;
|
|
248
289
|
}
|
|
290
|
+
if (skippedNamespaces instanceof Set && skippedNamespaces.has(entry?.relation?.namespace)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
249
293
|
|
|
250
294
|
if (!includeWasExplicit && !shouldHydrateByMode(entry)) {
|
|
251
295
|
return null;
|
|
@@ -353,7 +397,10 @@ function resolveLookupDepthRuntime(runtime = {}, repositoryOptions = {}, callOpt
|
|
|
353
397
|
}
|
|
354
398
|
|
|
355
399
|
function buildLookupGroupKey(relation = {}) {
|
|
356
|
-
|
|
400
|
+
if (relation?.kind === "collection") {
|
|
401
|
+
return `collection::${relation.namespace}::${relation.foreignKey}::${relation.parentValueKey}`;
|
|
402
|
+
}
|
|
403
|
+
return `lookup::${relation.namespace}::${relation.valueKey}`;
|
|
357
404
|
}
|
|
358
405
|
|
|
359
406
|
function normalizeLookupRelationValues(records = [], entries = [], childIncludeByKey = {}) {
|
|
@@ -379,7 +426,10 @@ function normalizeLookupRelationValues(records = [], entries = [], childIncludeB
|
|
|
379
426
|
const seen = new Set();
|
|
380
427
|
for (const record of records) {
|
|
381
428
|
for (const entry of group.entries) {
|
|
382
|
-
const
|
|
429
|
+
const relation = entry?.relation || {};
|
|
430
|
+
const rawValue = relation.kind === "collection"
|
|
431
|
+
? record?.[relation.parentValueKey]
|
|
432
|
+
: record?.[entry.key];
|
|
383
433
|
const normalized = normalizeLookupIdentifier(rawValue);
|
|
384
434
|
if (!normalized || seen.has(normalized)) {
|
|
385
435
|
continue;
|
|
@@ -437,6 +487,54 @@ function normalizeLookupProvider(provider, relation = {}, { context = "crudRepos
|
|
|
437
487
|
return provider;
|
|
438
488
|
}
|
|
439
489
|
|
|
490
|
+
function resolveLookupProviderOwnershipFilter(provider = {}, { context = "crudRepository" } = {}) {
|
|
491
|
+
const normalizedOwnershipFilter = normalizeText(provider?.ownershipFilter).toLowerCase();
|
|
492
|
+
if (!normalizedOwnershipFilter) {
|
|
493
|
+
return "";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (LOOKUP_PROVIDER_OWNERSHIP_FILTER_SET.has(normalizedOwnershipFilter)) {
|
|
497
|
+
return normalizedOwnershipFilter;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
throw new TypeError(
|
|
501
|
+
`${context} lookup provider ownershipFilter must be one of: ${LOOKUP_PROVIDER_OWNERSHIP_FILTER_VALUES.join(", ")}.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveLookupVisibilityContext(
|
|
506
|
+
provider = {},
|
|
507
|
+
relation = {},
|
|
508
|
+
callOptions = {},
|
|
509
|
+
{ context = "crudRepository" } = {}
|
|
510
|
+
) {
|
|
511
|
+
const parentVisibilityContext = normalizeVisibilityContext(callOptions?.visibilityContext);
|
|
512
|
+
const providerOwnershipFilter = resolveLookupProviderOwnershipFilter(provider, {
|
|
513
|
+
context
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
if (!providerOwnershipFilter) {
|
|
517
|
+
if (parentVisibilityContext.visibility !== "public") {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`${context} lookup provider for namespace "${relation.namespace}" must declare ownershipFilter when parent visibility is "${parentVisibilityContext.visibility}".`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return callOptions?.visibilityContext;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const nextVisibilityContext = {
|
|
526
|
+
visibility: providerOwnershipFilter
|
|
527
|
+
};
|
|
528
|
+
if (providerOwnershipFilter === "workspace" || providerOwnershipFilter === "workspace_user") {
|
|
529
|
+
nextVisibilityContext.scopeOwnerId = parentVisibilityContext.scopeOwnerId;
|
|
530
|
+
}
|
|
531
|
+
if (providerOwnershipFilter === "user" || providerOwnershipFilter === "workspace_user") {
|
|
532
|
+
nextVisibilityContext.userOwnerId = parentVisibilityContext.userOwnerId;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return nextVisibilityContext;
|
|
536
|
+
}
|
|
537
|
+
|
|
440
538
|
function buildLookupRecordMap(records = [], valueKey = "") {
|
|
441
539
|
const lookupMap = new Map();
|
|
442
540
|
for (const record of Array.isArray(records) ? records : []) {
|
|
@@ -449,6 +547,47 @@ function buildLookupRecordMap(records = [], valueKey = "") {
|
|
|
449
547
|
return lookupMap;
|
|
450
548
|
}
|
|
451
549
|
|
|
550
|
+
function buildLookupCollectionMap(records = [], foreignKey = "") {
|
|
551
|
+
const collectionMap = new Map();
|
|
552
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
553
|
+
const ownerId = normalizeLookupIdentifier(record?.[foreignKey]);
|
|
554
|
+
if (!ownerId) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const currentRecords = collectionMap.get(ownerId);
|
|
558
|
+
if (currentRecords) {
|
|
559
|
+
currentRecords.push(record);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
collectionMap.set(ownerId, [record]);
|
|
563
|
+
}
|
|
564
|
+
return collectionMap;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveLookupVisitedNamespaces(runtime = {}, callOptions = {}) {
|
|
568
|
+
const namespaces = [];
|
|
569
|
+
const seen = new Set();
|
|
570
|
+
|
|
571
|
+
function appendNamespace(value) {
|
|
572
|
+
const normalized = normalizeCrudLookupNamespace(value);
|
|
573
|
+
if (!normalized || seen.has(normalized)) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
seen.add(normalized);
|
|
577
|
+
namespaces.push(normalized);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const sourceVisitedNamespaces = Array.isArray(callOptions?.lookupVisitedNamespaces)
|
|
581
|
+
? callOptions.lookupVisitedNamespaces
|
|
582
|
+
: [];
|
|
583
|
+
for (const namespace of sourceVisitedNamespaces) {
|
|
584
|
+
appendNamespace(namespace);
|
|
585
|
+
}
|
|
586
|
+
appendNamespace(runtime?.namespace);
|
|
587
|
+
|
|
588
|
+
return namespaces;
|
|
589
|
+
}
|
|
590
|
+
|
|
452
591
|
async function hydrateCrudLookupRecords(
|
|
453
592
|
records = [],
|
|
454
593
|
runtime = {},
|
|
@@ -472,11 +611,13 @@ async function hydrateCrudLookupRecords(
|
|
|
472
611
|
const lookupContainerKey = normalizeCrudLookupContainerKey(runtime?.containerKey, {
|
|
473
612
|
context: `${runtime?.context || "crudRepository"} lookup runtime container key`
|
|
474
613
|
});
|
|
614
|
+
const visitedNamespaces = resolveLookupVisitedNamespaces(runtime, callOptions);
|
|
475
615
|
|
|
476
616
|
const lookupPlan = buildLookupHydrationPlan(runtime, include, {
|
|
477
617
|
mode,
|
|
478
618
|
context: runtime?.context || "crudRepository",
|
|
479
|
-
includeWasExplicit: normalizeText(include).length > 0
|
|
619
|
+
includeWasExplicit: normalizeText(include).length > 0,
|
|
620
|
+
skippedNamespaces: new Set(visitedNamespaces)
|
|
480
621
|
});
|
|
481
622
|
const selectedEntries = lookupPlan.entries;
|
|
482
623
|
if (selectedEntries.length < 1) {
|
|
@@ -498,17 +639,32 @@ async function hydrateCrudLookupRecords(
|
|
|
498
639
|
const provider = normalizeLookupProvider(resolveLookupProvider(group.relation), group.relation, {
|
|
499
640
|
context: runtime?.context || "crudRepository"
|
|
500
641
|
});
|
|
642
|
+
const childVisibilityContext = resolveLookupVisibilityContext(
|
|
643
|
+
provider,
|
|
644
|
+
group.relation,
|
|
645
|
+
callOptions,
|
|
646
|
+
{
|
|
647
|
+
context: runtime?.context || "crudRepository"
|
|
648
|
+
}
|
|
649
|
+
);
|
|
501
650
|
const childInclude = resolveGroupChildInclude(group);
|
|
651
|
+
const groupValueKey = group?.relation?.kind === "collection"
|
|
652
|
+
? group.relation.foreignKey
|
|
653
|
+
: group.relation.valueKey;
|
|
502
654
|
const groupRecords = await provider.listByIds(group.values, {
|
|
503
655
|
...callOptions,
|
|
656
|
+
visibilityContext: childVisibilityContext,
|
|
504
657
|
include: childInclude,
|
|
505
658
|
lookupDepth: depthRuntime.depth + 1,
|
|
506
659
|
lookupMaxDepth: depthRuntime.maxDepth,
|
|
507
|
-
|
|
660
|
+
lookupVisitedNamespaces: visitedNamespaces,
|
|
661
|
+
valueKey: groupValueKey
|
|
508
662
|
});
|
|
509
663
|
groupRecordMaps.set(
|
|
510
664
|
buildLookupGroupKey(group.relation),
|
|
511
|
-
|
|
665
|
+
group?.relation?.kind === "collection"
|
|
666
|
+
? buildLookupCollectionMap(groupRecords, group.relation.foreignKey)
|
|
667
|
+
: buildLookupRecordMap(groupRecords, group.relation.valueKey)
|
|
512
668
|
);
|
|
513
669
|
}
|
|
514
670
|
|
|
@@ -525,6 +681,11 @@ async function hydrateCrudLookupRecords(
|
|
|
525
681
|
for (const entry of selectedEntries) {
|
|
526
682
|
const relation = entry.relation;
|
|
527
683
|
const lookupMap = groupRecordMaps.get(buildLookupGroupKey(relation)) || new Map();
|
|
684
|
+
if (relation.kind === "collection") {
|
|
685
|
+
const ownerId = normalizeLookupIdentifier(baseRecord?.[relation.parentValueKey]);
|
|
686
|
+
nextLookups[entry.key] = ownerId ? (lookupMap.get(ownerId) || []) : [];
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
528
689
|
const lookupId = normalizeLookupIdentifier(baseRecord?.[entry.key]);
|
|
529
690
|
if (!lookupId) {
|
|
530
691
|
nextLookups[entry.key] = null;
|