@jskit-ai/crud-core 0.1.27 → 0.1.29

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.
@@ -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.27",
4
+ version: "0.1.29",
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.27"
29
+ "@jskit-ai/crud-core": "0.1.29"
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.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -10,15 +10,25 @@
10
10
  "./client/composables/createCrudClientSupport": "./src/client/composables/createCrudClientSupport.js",
11
11
  "./client/composables/crudClientSupportHelpers": "./src/client/composables/crudClientSupportHelpers.js",
12
12
  "./client/composables/useCrudRealtimeInvalidation": "./src/client/composables/useCrudRealtimeInvalidation.js",
13
+ "./shared/crudFieldMetaSupport": "./src/shared/crudFieldMetaSupport.js",
14
+ "./shared/crudNamespaceSupport": "./src/shared/crudNamespaceSupport.js",
13
15
  "./server/repositorySupport": "./src/server/repositorySupport.js",
14
- "./server/crudModuleConfig": "./src/server/crudModuleConfig.js"
16
+ "./server/repositoryMethods": "./src/server/repositoryMethods.js",
17
+ "./server/createCrudRepositoryFromResource": "./src/server/createCrudRepositoryFromResource.js",
18
+ "./server/lookupProviders": "./src/server/lookupProviders.js",
19
+ "./server/serviceEvents": "./src/server/serviceEvents.js",
20
+ "./server/fieldAccess": "./src/server/fieldAccess.js",
21
+ "./server/createCrudServiceFromResource": "./src/server/createCrudServiceFromResource.js",
22
+ "./server/crudModuleConfig": "./src/server/crudModuleConfig.js",
23
+ "./server/listQueryValidators": "./src/server/listQueryValidators.js"
15
24
  },
16
25
  "dependencies": {
17
26
  "@tanstack/vue-query": "^5.90.5",
18
- "@jskit-ai/kernel": "0.1.19",
19
- "@jskit-ai/realtime": "0.1.18",
20
- "@jskit-ai/shell-web": "0.1.18",
21
- "@jskit-ai/users-core": "0.1.28",
22
- "@jskit-ai/users-web": "0.1.33"
27
+ "@jskit-ai/kernel": "0.1.21",
28
+ "@jskit-ai/realtime": "0.1.20",
29
+ "@jskit-ai/shell-web": "0.1.20",
30
+ "@jskit-ai/users-core": "0.1.30",
31
+ "@jskit-ai/users-web": "0.1.35",
32
+ "typebox": "^1.0.81"
23
33
  }
24
34
  }
@@ -1,19 +1,14 @@
1
- import { normalizeLowerText, normalizeText, normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
1
+ import { normalizeText, normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import { normalizeRouteVisibilityToken } from "@jskit-ai/kernel/shared/support/visibility";
3
3
  import { formatDateTime } from "@jskit-ai/kernel/shared/support";
4
+ import {
5
+ requireCrudNamespace,
6
+ resolveCrudRecordChangedEvent
7
+ } from "../../shared/crudNamespaceSupport.js";
4
8
 
5
9
  const DEFAULT_CRUD_OWNERSHIP_FILTER = "workspace";
6
10
  const ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
7
11
 
8
- function requireCrudNamespace(namespace, { context = "resolveCrudClientConfig" } = {}) {
9
- const normalizedNamespace = normalizeLowerText(namespace);
10
- if (!normalizedNamespace) {
11
- throw new TypeError(`${context} requires a non-empty namespace.`);
12
- }
13
-
14
- return normalizedNamespace;
15
- }
16
-
17
12
  function normalizeRelativePath(value, { context = "resolveCrudClientConfig" } = {}) {
18
13
  const raw = normalizeText(value);
19
14
  if (!raw) {
@@ -77,13 +72,6 @@ function crudScopeQueryKey(namespace = "") {
77
72
  return Object.freeze(["crud", normalizeQueryToken(namespace)]);
78
73
  }
79
74
 
80
- function resolveCrudRecordChangedEvent(namespace = "") {
81
- const normalizedNamespace = requireCrudNamespace(namespace, {
82
- context: "resolveCrudRecordChangedEvent"
83
- });
84
- return `${normalizedNamespace.replace(/-/g, "_")}.record.changed`;
85
- }
86
-
87
75
  async function invalidateCrudQueries(queryClient, namespace = "") {
88
76
  if (!queryClient || typeof queryClient.invalidateQueries !== "function") {
89
77
  throw new TypeError("invalidateCrudQueries requires queryClient.invalidateQueries().");
@@ -0,0 +1,57 @@
1
+ import {
2
+ createCrudRepositoryRuntime,
3
+ crudRepositoryList,
4
+ crudRepositoryFindById,
5
+ crudRepositoryListByIds,
6
+ crudRepositoryCreate,
7
+ crudRepositoryUpdateById,
8
+ crudRepositoryDeleteById
9
+ } from "./repositoryMethods.js";
10
+
11
+ function createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {} } = {}) {
12
+ const runtime = createCrudRepositoryRuntime(resource, {
13
+ context,
14
+ list
15
+ });
16
+
17
+ return function createRepository(knex, options = {}) {
18
+ if (typeof knex !== "function") {
19
+ throw new TypeError("crudRepository requires knex.");
20
+ }
21
+
22
+ async function listRecords(query = {}, callOptions = {}) {
23
+ return crudRepositoryList(runtime, knex, query, options, callOptions);
24
+ }
25
+
26
+ async function findById(recordId, callOptions = {}) {
27
+ return crudRepositoryFindById(runtime, knex, recordId, options, callOptions);
28
+ }
29
+
30
+ async function listByIds(ids = [], callOptions = {}) {
31
+ return crudRepositoryListByIds(runtime, knex, ids, options, callOptions);
32
+ }
33
+
34
+ async function create(payload = {}, callOptions = {}) {
35
+ return crudRepositoryCreate(runtime, knex, payload, options, callOptions);
36
+ }
37
+
38
+ async function updateById(recordId, patch = {}, callOptions = {}) {
39
+ return crudRepositoryUpdateById(runtime, knex, recordId, patch, options, callOptions);
40
+ }
41
+
42
+ async function deleteById(recordId, callOptions = {}) {
43
+ return crudRepositoryDeleteById(runtime, knex, recordId, options, callOptions);
44
+ }
45
+
46
+ return Object.freeze({
47
+ list: listRecords,
48
+ findById,
49
+ listByIds,
50
+ create,
51
+ updateById,
52
+ deleteById
53
+ });
54
+ };
55
+ }
56
+
57
+ export { createCrudRepositoryFromResource };
@@ -0,0 +1,101 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { requireCrudNamespace } from "../shared/crudNamespaceSupport.js";
3
+ import { createCrudFieldAccessRuntime } from "./fieldAccess.js";
4
+ import { createCrudServiceEvents } from "./serviceEvents.js";
5
+
6
+ function createCrudServiceFromResource(resource = {}, { context = "crudService" } = {}) {
7
+ const namespace = requireCrudNamespace(resource?.resource, { context: `${context} resource.resource` });
8
+ const baseServiceEvents = createCrudServiceEvents(resource, { context });
9
+ const fieldAccessRuntime = createCrudFieldAccessRuntime(resource, { context });
10
+
11
+ function createBaseService({ repository, fieldAccess = {} } = {}) {
12
+ if (!repository) {
13
+ throw new Error(`${context} requires repository.`);
14
+ }
15
+
16
+ async function listRecords(query = {}, options = {}) {
17
+ const result = await repository.list(query, options);
18
+ return fieldAccessRuntime.filterReadableListResult(result, fieldAccess, {
19
+ action: "list",
20
+ query,
21
+ options,
22
+ context: options?.context
23
+ });
24
+ }
25
+
26
+ async function getRecord(recordId, options = {}) {
27
+ const record = await repository.findById(recordId, options);
28
+ if (!record) {
29
+ throw new AppError(404, "Record not found.");
30
+ }
31
+
32
+ return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
33
+ action: "view",
34
+ recordId,
35
+ options,
36
+ context: options?.context
37
+ });
38
+ }
39
+
40
+ async function createRecord(payload = {}, options = {}) {
41
+ const writablePayload = await fieldAccessRuntime.enforceWritablePayload(payload, fieldAccess, {
42
+ action: "create",
43
+ payload,
44
+ options,
45
+ context: options?.context
46
+ });
47
+ const record = await repository.create(writablePayload, options);
48
+ if (!record) {
49
+ throw new Error(`${namespace}Service could not load the created record.`);
50
+ }
51
+ return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
52
+ action: "create",
53
+ options,
54
+ context: options?.context
55
+ });
56
+ }
57
+
58
+ async function updateRecord(recordId, payload = {}, options = {}) {
59
+ const writablePayload = await fieldAccessRuntime.enforceWritablePayload(payload, fieldAccess, {
60
+ action: "update",
61
+ recordId,
62
+ payload,
63
+ options,
64
+ context: options?.context
65
+ });
66
+ const record = await repository.updateById(recordId, writablePayload, options);
67
+ if (!record) {
68
+ throw new AppError(404, "Record not found.");
69
+ }
70
+ return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
71
+ action: "update",
72
+ recordId,
73
+ options,
74
+ context: options?.context
75
+ });
76
+ }
77
+
78
+ async function deleteRecord(recordId, options = {}) {
79
+ const deleted = await repository.deleteById(recordId, options);
80
+ if (!deleted) {
81
+ throw new AppError(404, "Record not found.");
82
+ }
83
+ return deleted;
84
+ }
85
+
86
+ return Object.freeze({
87
+ listRecords,
88
+ getRecord,
89
+ createRecord,
90
+ updateRecord,
91
+ deleteRecord
92
+ });
93
+ }
94
+
95
+ return Object.freeze({
96
+ createBaseService,
97
+ baseServiceEvents
98
+ });
99
+ }
100
+
101
+ export { createCrudServiceFromResource };
@@ -1,11 +1,15 @@
1
1
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
+ import {
4
+ normalizeCrudNamespace,
5
+ requireCrudNamespace
6
+ } from "../shared/crudNamespaceSupport.js";
3
7
  import {
4
8
  resolveApiBasePath
5
9
  } from "@jskit-ai/users-core/shared/support/usersApiPaths";
6
10
  import {
7
11
  USERS_ROUTE_VISIBILITY_LEVELS,
8
- normalizeScopedRouteVisibility,
12
+ checkRouteVisibility,
9
13
  isWorkspaceVisibility
10
14
  } from "@jskit-ai/users-core/shared/support/usersVisibility";
11
15
 
@@ -25,16 +29,13 @@ function asRecord(value) {
25
29
  return value;
26
30
  }
27
31
 
28
- function normalizeCrudNamespace(value) {
29
- return normalizeText(value)
30
- .toLowerCase()
31
- .replace(/[^a-z0-9-]+/g, "-")
32
- .replace(/-+/g, "-")
33
- .replace(/^-+|-+$/g, "");
34
- }
35
-
36
32
  function normalizeCrudOwnershipFilter(value, { fallback = DEFAULT_OWNERSHIP_FILTER } = {}) {
37
- return normalizeScopedRouteVisibility(value, { fallback });
33
+ const normalizedValue = normalizeText(value).toLowerCase();
34
+ const normalizedFallback = normalizeText(fallback).toLowerCase();
35
+ const resolved = normalizedValue || normalizedFallback;
36
+ return checkRouteVisibility(resolved, {
37
+ context: "normalizeCrudOwnershipFilter ownershipFilter"
38
+ });
38
39
  }
39
40
 
40
41
  function normalizeCrudRequestedOwnershipFilter(value, { fallback = CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO } = {}) {
@@ -51,15 +52,6 @@ function normalizeCrudRequestedOwnershipFilter(value, { fallback = CRUD_REQUESTE
51
52
  return CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO;
52
53
  }
53
54
 
54
- function requireCrudNamespace(namespace, { context = "CRUD config" } = {}) {
55
- const normalizedNamespace = normalizeCrudNamespace(namespace);
56
- if (!normalizedNamespace) {
57
- throw new TypeError(`${context} requires a non-empty namespace.`);
58
- }
59
-
60
- return normalizedNamespace;
61
- }
62
-
63
55
  function resolveCrudNamespacePath(namespace = "") {
64
56
  const normalizedNamespace = requireCrudNamespace(namespace, {
65
57
  context: "resolveCrudNamespacePath"
@@ -208,8 +200,8 @@ function resolveCrudSurfacePolicy(
208
200
  const ownershipFilter =
209
201
  requestedOwnershipFilter === CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO
210
202
  ? resolveOwnershipFilterFromSurfaceDefinition(surfaceDefinition)
211
- : normalizeScopedRouteVisibility(requestedOwnershipFilter, {
212
- fallback: "public"
203
+ : checkRouteVisibility(requestedOwnershipFilter, {
204
+ context: `${context} ownershipFilter`
213
205
  });
214
206
 
215
207
  if (isWorkspaceVisibility(ownershipFilter) && surfaceDefinition.requiresWorkspace !== true) {
@@ -0,0 +1,316 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
4
+
5
+ function isSchemaNullable(schema = {}) {
6
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
7
+ return false;
8
+ }
9
+
10
+ const schemaType = schema.type;
11
+ if (typeof schemaType === "string" && normalizeText(schemaType).toLowerCase() === "null") {
12
+ return true;
13
+ }
14
+ if (Array.isArray(schemaType)) {
15
+ const hasNullType = schemaType.some((entry) => normalizeText(entry).toLowerCase() === "null");
16
+ if (hasNullType) {
17
+ return true;
18
+ }
19
+ }
20
+
21
+ const variants = [];
22
+ if (Array.isArray(schema.anyOf)) {
23
+ variants.push(...schema.anyOf);
24
+ }
25
+ if (Array.isArray(schema.oneOf)) {
26
+ variants.push(...schema.oneOf);
27
+ }
28
+
29
+ return variants.some((entry) => isSchemaNullable(entry));
30
+ }
31
+
32
+ function normalizeFieldSet(value, { context = "crudFieldAccess", label = "field list" } = {}) {
33
+ if (value == null || value === "*") {
34
+ return null;
35
+ }
36
+
37
+ const rawValues = value instanceof Set
38
+ ? [...value]
39
+ : Array.isArray(value)
40
+ ? value
41
+ : null;
42
+ if (!rawValues) {
43
+ throw new TypeError(`${context} ${label} must be an array, set, or "*".`);
44
+ }
45
+
46
+ const normalizedKeys = rawValues
47
+ .map((entry) => normalizeText(entry))
48
+ .filter(Boolean);
49
+
50
+ return new Set(normalizedKeys);
51
+ }
52
+
53
+ async function resolveFieldSet(resolver, input = {}, { context = "crudFieldAccess", label = "field list" } = {}) {
54
+ const resolvedResolver = resolver;
55
+ if (resolvedResolver == null) {
56
+ return null;
57
+ }
58
+
59
+ const resolvedValue = typeof resolvedResolver === "function"
60
+ ? await resolvedResolver(input)
61
+ : resolvedResolver;
62
+
63
+ return normalizeFieldSet(resolvedValue, { context, label });
64
+ }
65
+
66
+ function resolveWriteMode(fieldAccess = {}, { context = "crudFieldAccess" } = {}) {
67
+ const writeMode = normalizeText(fieldAccess?.writeMode || "throw").toLowerCase();
68
+ if (writeMode === "throw" || writeMode === "strip") {
69
+ return writeMode;
70
+ }
71
+
72
+ throw new TypeError(`${context} fieldAccess.writeMode must be "throw" or "strip".`);
73
+ }
74
+
75
+ function buildOutputFieldRules(resource = {}) {
76
+ const viewOutputSchema = resource?.operations?.view?.outputValidator?.schema;
77
+ if (!viewOutputSchema || typeof viewOutputSchema !== "object" || Array.isArray(viewOutputSchema)) {
78
+ return null;
79
+ }
80
+
81
+ const outputProperties = normalizeObject(viewOutputSchema.properties);
82
+ const requiredFields = new Set(
83
+ (Array.isArray(viewOutputSchema.required) ? viewOutputSchema.required : [])
84
+ .map((entry) => normalizeText(entry))
85
+ .filter(Boolean)
86
+ );
87
+ const fieldRules = new Map();
88
+
89
+ for (const [fieldKey, fieldSchemaRaw] of Object.entries(outputProperties)) {
90
+ const normalizedFieldKey = normalizeText(fieldKey);
91
+ if (!normalizedFieldKey) {
92
+ continue;
93
+ }
94
+
95
+ const fieldSchema = normalizeObject(fieldSchemaRaw);
96
+ fieldRules.set(
97
+ normalizedFieldKey,
98
+ Object.freeze({
99
+ required: requiredFields.has(normalizedFieldKey),
100
+ nullable: isSchemaNullable(fieldSchema),
101
+ hasDefault: Object.hasOwn(fieldSchema, "default"),
102
+ defaultValue: fieldSchema.default
103
+ })
104
+ );
105
+ }
106
+
107
+ return Object.freeze({
108
+ fieldRules: new Map(fieldRules)
109
+ });
110
+ }
111
+
112
+ function resolveRoleFromFieldAccessInput(input = {}) {
113
+ const role = normalizeText(input?.context?.auth?.role).toLowerCase();
114
+ return role || "default";
115
+ }
116
+
117
+ function resolveActionFromFieldAccessInput(input = {}) {
118
+ const action = normalizeText(input?.action).toLowerCase();
119
+ return action || "*";
120
+ }
121
+
122
+ function resolveOperationPolicyValue(operationPolicy, input = {}, action = "*") {
123
+ if (typeof operationPolicy === "function") {
124
+ return operationPolicy(input);
125
+ }
126
+ if (
127
+ operationPolicy == null ||
128
+ operationPolicy === "*" ||
129
+ Array.isArray(operationPolicy) ||
130
+ operationPolicy instanceof Set
131
+ ) {
132
+ return operationPolicy;
133
+ }
134
+ if (typeof operationPolicy !== "object" || Array.isArray(operationPolicy)) {
135
+ return null;
136
+ }
137
+
138
+ if (Object.hasOwn(operationPolicy, action)) {
139
+ return operationPolicy[action];
140
+ }
141
+ if (Object.hasOwn(operationPolicy, "*")) {
142
+ return operationPolicy["*"];
143
+ }
144
+ if (Object.hasOwn(operationPolicy, "all")) {
145
+ return operationPolicy.all;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function resolveRoleMatrixPolicy(matrix = {}, operation = "readable", input = {}) {
151
+ const sourceMatrix = normalizeObject(matrix);
152
+ const role = resolveRoleFromFieldAccessInput(input);
153
+ const action = resolveActionFromFieldAccessInput(input);
154
+
155
+ const rolePolicy = normalizeObject(sourceMatrix[role]);
156
+ const roleValue = resolveOperationPolicyValue(rolePolicy[operation], input, action);
157
+ if (roleValue != null) {
158
+ return roleValue;
159
+ }
160
+
161
+ const defaultPolicy = normalizeObject(sourceMatrix.default);
162
+ return resolveOperationPolicyValue(defaultPolicy[operation], input, action);
163
+ }
164
+
165
+ function createFieldAccessForRoleMatrix(matrix = {}, { context = "crudFieldAccess" } = {}) {
166
+ const sourceMatrix = normalizeObject(matrix);
167
+ const writeMode = resolveWriteMode(
168
+ { writeMode: sourceMatrix.writeMode },
169
+ { context: `${context} createFieldAccessForRoleMatrix` }
170
+ );
171
+
172
+ return Object.freeze({
173
+ readable(input = {}) {
174
+ return resolveRoleMatrixPolicy(sourceMatrix, "readable", input);
175
+ },
176
+ writable(input = {}) {
177
+ return resolveRoleMatrixPolicy(sourceMatrix, "writable", input);
178
+ },
179
+ writeMode
180
+ });
181
+ }
182
+
183
+ function applyReadableFieldPolicyToRecord(record, allowedFields, outputRules = null, { context = "crudFieldAccess" } = {}) {
184
+ if (!record || typeof record !== "object" || Array.isArray(record) || !allowedFields) {
185
+ return record;
186
+ }
187
+
188
+ const nextRecord = { ...record };
189
+ const fieldRules = outputRules?.fieldRules instanceof Map ? outputRules.fieldRules : null;
190
+
191
+ for (const fieldKey of Object.keys(nextRecord)) {
192
+ if (allowedFields.has(fieldKey)) {
193
+ continue;
194
+ }
195
+
196
+ const fieldRule = fieldRules?.get(fieldKey);
197
+ if (!fieldRule || fieldRule.required !== true) {
198
+ delete nextRecord[fieldKey];
199
+ continue;
200
+ }
201
+
202
+ if (fieldRule.nullable) {
203
+ nextRecord[fieldKey] = null;
204
+ continue;
205
+ }
206
+
207
+ if (fieldRule.hasDefault) {
208
+ nextRecord[fieldKey] = fieldRule.defaultValue;
209
+ continue;
210
+ }
211
+
212
+ throw new Error(
213
+ `${context} cannot redact required non-nullable field "${fieldKey}" without schema.default.`
214
+ );
215
+ }
216
+
217
+ return nextRecord;
218
+ }
219
+
220
+ function createCrudFieldAccessRuntime(resource = {}, { context = "crudFieldAccess" } = {}) {
221
+ const outputRules = buildOutputFieldRules(resource);
222
+
223
+ async function resolveReadableAllowedFields(fieldAccess = {}, input = {}) {
224
+ const allowedFields = await resolveFieldSet(
225
+ fieldAccess?.readable,
226
+ input,
227
+ {
228
+ context,
229
+ label: "fieldAccess.readable"
230
+ }
231
+ );
232
+ if (!allowedFields) {
233
+ return null;
234
+ }
235
+ if (!outputRules) {
236
+ throw new TypeError(`${context} requires resource.operations.view.outputValidator.schema for fieldAccess.readable.`);
237
+ }
238
+
239
+ return allowedFields;
240
+ }
241
+
242
+ async function enforceWritablePayload(payload = {}, fieldAccess = {}, input = {}) {
243
+ const allowedFields = await resolveFieldSet(
244
+ fieldAccess?.writable,
245
+ input,
246
+ {
247
+ context,
248
+ label: "fieldAccess.writable"
249
+ }
250
+ );
251
+ if (!allowedFields) {
252
+ return payload;
253
+ }
254
+ const sourcePayload = normalizeObjectInput(payload);
255
+
256
+ const writeMode = resolveWriteMode(fieldAccess, { context });
257
+ const filteredPayload = {};
258
+ const forbiddenFields = [];
259
+ for (const [fieldKey, fieldValue] of Object.entries(sourcePayload)) {
260
+ if (allowedFields.has(fieldKey)) {
261
+ filteredPayload[fieldKey] = fieldValue;
262
+ } else {
263
+ forbiddenFields.push(fieldKey);
264
+ }
265
+ }
266
+
267
+ if (forbiddenFields.length > 0 && writeMode === "throw") {
268
+ throw new AppError(403, `Write access denied for fields: ${forbiddenFields.join(", ")}.`);
269
+ }
270
+
271
+ return filteredPayload;
272
+ }
273
+
274
+ async function filterReadableRecord(record = null, fieldAccess = {}, input = {}) {
275
+ const allowedFields = await resolveReadableAllowedFields(fieldAccess, input);
276
+ if (!allowedFields) {
277
+ return record;
278
+ }
279
+
280
+ return applyReadableFieldPolicyToRecord(record, allowedFields, outputRules, {
281
+ context
282
+ });
283
+ }
284
+
285
+ async function filterReadableListResult(listResult = {}, fieldAccess = {}, input = {}) {
286
+ const allowedFields = await resolveReadableAllowedFields(fieldAccess, input);
287
+ if (!allowedFields) {
288
+ return listResult;
289
+ }
290
+
291
+ const sourceList = normalizeObject(listResult);
292
+ const sourceItems = Array.isArray(sourceList.items) ? sourceList.items : [];
293
+ if (sourceItems.length < 1) {
294
+ return sourceList;
295
+ }
296
+ const filteredItems = sourceItems.map((record) =>
297
+ applyReadableFieldPolicyToRecord(record, allowedFields, outputRules, {
298
+ context
299
+ })
300
+ );
301
+
302
+ return {
303
+ ...sourceList,
304
+ items: filteredItems
305
+ };
306
+ }
307
+
308
+ return Object.freeze({
309
+ enforceWritablePayload,
310
+ filterReadableRecord,
311
+ filterReadableListResult
312
+ });
313
+ }
314
+
315
+ export { createCrudFieldAccessRuntime };
316
+ export { createFieldAccessForRoleMatrix };