@jskit-ai/crud-core 0.1.63 → 0.1.64

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,53 +1,38 @@
1
- import { Type } from "typebox";
1
+ import { createSchema } from "json-rest-schema";
2
2
  import {
3
- normalizeObjectInput,
4
- positiveIntegerValidator,
5
3
  cursorPaginationQueryValidator
6
4
  } from "@jskit-ai/kernel/shared/validators";
7
5
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
8
6
  import { resolveCrudParentFilterKeys as resolveSharedCrudParentFilterKeys } from "@jskit-ai/kernel/shared/support/crudLookup";
9
7
 
10
8
  const listSearchQueryValidator = Object.freeze({
11
- schema: Type.Object(
12
- {
13
- q: Type.Optional(Type.String({ minLength: 0 }))
14
- },
15
- { additionalProperties: false }
16
- ),
17
- normalize(payload = {}) {
18
- const source = normalizeObjectInput(payload);
19
- if (!Object.hasOwn(source, "q")) {
20
- return {};
9
+ schema: createSchema({
10
+ q: {
11
+ type: "string",
12
+ required: false
21
13
  }
22
-
23
- return {
24
- q: normalizeText(source.q)
25
- };
26
- }
14
+ }),
15
+ mode: "patch"
27
16
  });
28
17
 
29
18
  const lookupIncludeQueryValidator = Object.freeze({
30
- schema: Type.Object(
31
- {
32
- include: Type.Optional(Type.String({ minLength: 0 }))
33
- },
34
- { additionalProperties: false }
35
- ),
36
- normalize(payload = {}) {
37
- const source = normalizeObjectInput(payload);
38
- if (!Object.hasOwn(source, "include")) {
39
- return {};
19
+ schema: createSchema({
20
+ include: {
21
+ type: "string",
22
+ required: false
40
23
  }
41
-
42
- return {
43
- include: normalizeText(source.include)
44
- };
45
- }
24
+ }),
25
+ mode: "patch"
46
26
  });
47
27
 
48
28
  function resolveCrudListUsesOrderedCursor(list = {}) {
49
- const orderBy = Array.isArray(list?.orderBy) ? list.orderBy : [];
50
- for (const entry of orderBy) {
29
+ const entries = Array.isArray(list?.orderBy)
30
+ ? list.orderBy
31
+ : list?.orderBy == null
32
+ ? []
33
+ : [list.orderBy];
34
+
35
+ for (const entry of entries) {
51
36
  if (typeof entry === "string" && normalizeText(entry)) {
52
37
  return true;
53
38
  }
@@ -65,32 +50,20 @@ function createCrudCursorPaginationQueryValidator(list = {}) {
65
50
  }
66
51
 
67
52
  return Object.freeze({
68
- schema: Type.Object(
69
- {
70
- cursor: Type.Optional(
71
- Type.Union([
72
- positiveIntegerValidator.schema,
73
- Type.String({ minLength: 1 })
74
- ])
75
- ),
76
- limit: Type.Optional(positiveIntegerValidator.schema)
53
+ schema: createSchema({
54
+ cursor: {
55
+ type: "string",
56
+ required: false,
57
+ minLength: 1
77
58
  },
78
- { additionalProperties: false }
79
- ),
80
- normalize(payload = {}) {
81
- const source = normalizeObjectInput(payload);
82
- const normalized = {};
83
-
84
- if (Object.hasOwn(source, "cursor")) {
85
- normalized.cursor = normalizeText(source.cursor);
59
+ limit: {
60
+ type: "number",
61
+ required: false,
62
+ min: 1,
63
+ unsigned: true
86
64
  }
87
-
88
- if (Object.hasOwn(source, "limit")) {
89
- normalized.limit = positiveIntegerValidator.normalize(source.limit);
90
- }
91
-
92
- return normalized;
93
- }
65
+ }),
66
+ mode: "patch"
94
67
  });
95
68
  }
96
69
 
@@ -102,27 +75,16 @@ function createCrudParentFilterQueryValidator(resource = {}) {
102
75
  const keys = resolveCrudParentFilterKeys(resource);
103
76
  const schemaProperties = {};
104
77
  for (const key of keys) {
105
- schemaProperties[key] = Type.Optional(Type.String({ minLength: 1 }));
78
+ schemaProperties[key] = {
79
+ type: "string",
80
+ required: false,
81
+ minLength: 1
82
+ };
106
83
  }
107
84
 
108
85
  return Object.freeze({
109
- schema: Type.Object(schemaProperties, { additionalProperties: false }),
110
- normalize(payload = {}) {
111
- const source = normalizeObjectInput(payload);
112
- const normalized = {};
113
- for (const key of keys) {
114
- if (!Object.hasOwn(source, key)) {
115
- continue;
116
- }
117
-
118
- const value = normalizeText(source[key]);
119
- if (value) {
120
- normalized[key] = value;
121
- }
122
- }
123
-
124
- return normalized;
125
- }
86
+ schema: createSchema(schemaProperties),
87
+ mode: "patch"
126
88
  });
127
89
  }
128
90
 
@@ -6,6 +6,7 @@ import {
6
6
  normalizeCrudLookupContainerKey,
7
7
  resolveCrudLookupContainerKey
8
8
  } from "@jskit-ai/kernel/shared/support/crudLookup";
9
+ import { buildCrudFieldContractMap } from "@jskit-ai/kernel/shared/support/crudFieldContract";
9
10
  import { normalizeCrudLookupApiPath } from "./lookupPathSupport.js";
10
11
 
11
12
  const DEFAULT_LOOKUP_INCLUDE = "*";
@@ -136,7 +137,9 @@ function createCrudLookupRuntime(resource = {}, { outputKeys = [] } = {}) {
136
137
  .filter(Boolean)
137
138
  );
138
139
 
139
- const sourceEntries = Array.isArray(resource?.fieldMeta) ? resource.fieldMeta : [];
140
+ const sourceEntries = Object.values(buildCrudFieldContractMap(resource, {
141
+ context: "crud lookup runtime field contract"
142
+ }));
140
143
  const lookupEntries = [];
141
144
  const seenKeys = new Set();
142
145
 
@@ -6,23 +6,25 @@ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
6
6
  import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
7
7
  import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
8
8
  import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
9
+ import { normalizeSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
10
+ import {
11
+ buildCrudFieldContractMap,
12
+ resolveCrudFieldSchemaProperties,
13
+ CRUD_FIELD_STORAGE_COLUMN,
14
+ CRUD_FIELD_STORAGE_VIRTUAL,
15
+ CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC
16
+ } from "@jskit-ai/kernel/shared/support/crudFieldContract";
9
17
  import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
10
18
  import {
11
19
  resolveCrudLookupContainerKey,
12
20
  resolveCrudLookupFieldKeys
13
21
  } from "@jskit-ai/kernel/shared/support/crudLookup";
14
- import {
15
- CRUD_FIELD_REPOSITORY_STORAGE_COLUMN,
16
- CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC,
17
- CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL,
18
- isCrudRuntimeOutputOnlyFieldKey,
19
- normalizeCrudFieldRepositoryConfig
20
- } from "../shared/crudFieldMetaSupport.js";
22
+ import { isCrudRuntimeOutputOnlyFieldKey } from "../shared/crudFieldSupport.js";
21
23
 
22
24
  const DEFAULT_LIST_LIMIT = 20;
23
25
  const MAX_LIST_LIMIT = 100;
24
26
  const CRUD_WRITE_SERIALIZERS = Object.freeze({
25
- [CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC]: (value) => toDatabaseDateTimeUtc(value)
27
+ [CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC]: (value) => toDatabaseDateTimeUtc(value)
26
28
  });
27
29
 
28
30
  function normalizeCrudListCursor(cursor = null, { allowEmpty = true } = {}) {
@@ -93,7 +95,7 @@ function buildRepositoryColumnMetadata({
93
95
  .filter(Boolean);
94
96
 
95
97
  const deriveMapping = (key) => {
96
- if (fieldStorageByKey?.[key] !== CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) {
98
+ if (fieldStorageByKey?.[key] !== CRUD_FIELD_STORAGE_COLUMN) {
97
99
  return null;
98
100
  }
99
101
  const column = resolveColumnName(key, columnOverrides);
@@ -116,105 +118,55 @@ function buildRepositoryColumnMetadata({
116
118
  });
117
119
  }
118
120
 
119
- function resolveOptionalObjectSchemaProperties(schema, options = {}) {
120
- if (!schema) {
121
+ function resolveOptionalFieldDefinitions(definition, options = {}) {
122
+ if (!definition) {
121
123
  return {};
122
124
  }
123
- return requireObjectSchemaProperties(schema, options);
125
+ return requireFieldDefinitions(definition, options);
124
126
  }
125
127
 
126
- function requireObjectSchemaProperties(schema, { context = "crudRepository", schemaLabel = "schema" } = {}) {
127
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
128
- throw new TypeError(`${context} requires ${schemaLabel} to be an object schema.`);
128
+ function requireFieldDefinitions(definition, {
129
+ context = "crudRepository",
130
+ schemaLabel = "schema definition",
131
+ defaultMode = "patch"
132
+ } = {}) {
133
+ const normalized = normalizeSchemaDefinition(definition, {
134
+ context: `${context} ${schemaLabel}`,
135
+ defaultMode
136
+ });
137
+ if (!normalized) {
138
+ throw new TypeError(`${context} requires ${schemaLabel}.`);
129
139
  }
130
140
 
131
- const properties = schema.properties;
141
+ const properties = resolveCrudFieldSchemaProperties(normalized, {
142
+ context: `${context} ${schemaLabel}`
143
+ });
132
144
  if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
133
- throw new TypeError(`${context} requires ${schemaLabel}.properties.`);
145
+ throw new TypeError(`${context} requires ${schemaLabel} field definitions.`);
134
146
  }
135
147
 
136
148
  return properties;
137
149
  }
138
150
 
139
- function normalizeResourceFieldMetaEntries(fieldMeta = []) {
140
- if (!Array.isArray(fieldMeta)) {
141
- return [];
142
- }
143
-
144
- const normalized = [];
145
- const seenKeys = new Set();
146
- for (const rawEntry of fieldMeta) {
147
- if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
148
- continue;
149
- }
150
-
151
- const key = normalizeText(rawEntry.key);
152
- if (!key || seenKeys.has(key)) {
153
- continue;
154
- }
155
- seenKeys.add(key);
156
- normalized.push(rawEntry);
157
- }
158
-
159
- return normalized;
151
+ function resolveFieldDefinitionType(definition = {}) {
152
+ return normalizeText(definition?.type).toLowerCase();
160
153
  }
161
154
 
162
- function schemaIncludesStringType(schema = {}) {
163
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
164
- return false;
165
- }
166
-
167
- const type = normalizeText(schema.type).toLowerCase();
168
- if (type === "string") {
169
- return true;
170
- }
171
-
172
- const variants = Array.isArray(schema.anyOf)
173
- ? schema.anyOf
174
- : Array.isArray(schema.oneOf)
175
- ? schema.oneOf
176
- : [];
177
- return variants.some((entry) => schemaIncludesStringType(entry));
155
+ function definitionIncludesStringType(definition = {}) {
156
+ return resolveFieldDefinitionType(definition) === "string";
178
157
  }
179
158
 
180
- function schemaIncludesDateTimeFormat(schema = {}) {
181
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
182
- return false;
183
- }
184
-
185
- if (normalizeText(schema.format).toLowerCase() === "date-time") {
186
- return true;
187
- }
188
-
189
- const variants = Array.isArray(schema.anyOf)
190
- ? schema.anyOf
191
- : Array.isArray(schema.oneOf)
192
- ? schema.oneOf
193
- : [];
194
- return variants.some((entry) => schemaIncludesDateTimeFormat(entry));
159
+ function definitionIncludesDateTimeType(definition = {}) {
160
+ return resolveFieldDefinitionType(definition) === "datetime";
195
161
  }
196
162
 
197
- function schemaIncludesRecordIdType(schema = {}) {
198
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
199
- return false;
200
- }
201
-
202
- const type = Array.isArray(schema.type)
203
- ? schema.type.map((entry) => normalizeText(entry).toLowerCase()).filter(Boolean)
204
- : normalizeText(schema.type).toLowerCase();
205
- const hasStringType = Array.isArray(type)
206
- ? type.includes("string")
207
- : type === "string";
208
- if (hasStringType && normalizeText(schema.pattern) === RECORD_ID_PATTERN) {
163
+ function definitionIncludesRecordIdType(definition = {}) {
164
+ const type = resolveFieldDefinitionType(definition);
165
+ if (type === "id") {
209
166
  return true;
210
167
  }
211
168
 
212
- const variants = Array.isArray(schema.anyOf)
213
- ? schema.anyOf
214
- : Array.isArray(schema.oneOf)
215
- ? schema.oneOf
216
- : [];
217
- return variants.some((entry) => schemaIncludesRecordIdType(entry));
169
+ return type === "string" && normalizeText(definition?.pattern) === RECORD_ID_PATTERN;
218
170
  }
219
171
 
220
172
  function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRepository" } = {}) {
@@ -227,20 +179,20 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
227
179
  throw new TypeError(`${context} requires resource.operations.`);
228
180
  }
229
181
 
230
- const outputSchema = operations?.view?.outputValidator?.schema;
231
- const writeSchema = operations?.create?.bodyValidator?.schema;
232
- const patchSchema = operations?.patch?.bodyValidator?.schema;
233
- const outputProperties = requireObjectSchemaProperties(outputSchema, {
182
+ const outputProperties = requireFieldDefinitions(operations?.view?.output, {
234
183
  context,
235
- schemaLabel: "operations.view.outputValidator.schema"
184
+ schemaLabel: "operations.view.output",
185
+ defaultMode: "replace"
236
186
  });
237
- const writeProperties = requireObjectSchemaProperties(writeSchema, {
187
+ const writeProperties = requireFieldDefinitions(operations?.create?.body, {
238
188
  context,
239
- schemaLabel: "operations.create.bodyValidator.schema"
189
+ schemaLabel: "operations.create.body",
190
+ defaultMode: "create"
240
191
  });
241
- const patchProperties = resolveOptionalObjectSchemaProperties(patchSchema, {
192
+ const patchProperties = resolveOptionalFieldDefinitions(operations?.patch?.body, {
242
193
  context,
243
- schemaLabel: "operations.patch.bodyValidator.schema"
194
+ schemaLabel: "operations.patch.body",
195
+ defaultMode: "patch"
244
196
  });
245
197
  const lookupContainerKey = resolveCrudLookupContainerKey(resource, {
246
198
  context: `${context} resource.contract.lookup.containerKey`
@@ -258,35 +210,33 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
258
210
  const fieldStorageByKey = {};
259
211
  const columnOverrides = {};
260
212
  const writeSerializerByKey = {};
261
- for (const entry of normalizeResourceFieldMetaEntries(resource.fieldMeta)) {
213
+ for (const entry of Object.values(buildCrudFieldContractMap(resource, {
214
+ context: `${context} resource field contract`
215
+ }))) {
262
216
  const key = normalizeText(entry.key);
263
217
  if (!key) {
264
218
  continue;
265
219
  }
266
- const repositoryConfig = normalizeCrudFieldRepositoryConfig(entry, {
267
- context: `${context} resource.fieldMeta`,
268
- fieldKey: key
269
- });
270
- fieldStorageByKey[key] = repositoryConfig.storage;
271
- if (repositoryConfig.column) {
272
- columnOverrides[key] = repositoryConfig.column;
220
+ fieldStorageByKey[key] = entry?.storage?.mode || CRUD_FIELD_STORAGE_COLUMN;
221
+ if (entry?.storage?.column) {
222
+ columnOverrides[key] = entry.storage.column;
273
223
  }
274
- if (repositoryConfig.writeSerializer) {
275
- writeSerializerByKey[key] = repositoryConfig.writeSerializer;
224
+ if (entry?.storage?.writeSerializer) {
225
+ writeSerializerByKey[key] = entry.storage.writeSerializer;
276
226
  }
277
227
  }
278
228
 
279
229
  for (const key of [...outputKeys, ...writeKeys]) {
280
230
  if (!fieldStorageByKey[key]) {
281
- fieldStorageByKey[key] = CRUD_FIELD_REPOSITORY_STORAGE_COLUMN;
231
+ fieldStorageByKey[key] = CRUD_FIELD_STORAGE_COLUMN;
282
232
  }
283
233
  }
284
234
 
285
235
  const virtualOutputKeys = [];
286
236
  const columnBackedOutputKeys = [];
287
237
  for (const key of outputKeys) {
288
- const storage = fieldStorageByKey[key] || CRUD_FIELD_REPOSITORY_STORAGE_COLUMN;
289
- if (storage === CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL) {
238
+ const storage = fieldStorageByKey[key] || CRUD_FIELD_STORAGE_COLUMN;
239
+ if (storage === CRUD_FIELD_STORAGE_VIRTUAL) {
290
240
  virtualOutputKeys.push(key);
291
241
  continue;
292
242
  }
@@ -296,22 +246,22 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
296
246
  for (const key of virtualOutputKeys) {
297
247
  if (Object.hasOwn(writeProperties, key)) {
298
248
  throw new Error(
299
- `${context} resource create schema field "${key}" cannot use repository.storage "virtual".`
249
+ `${context} resource create schema field "${key}" cannot use storage.virtual.`
300
250
  );
301
251
  }
302
252
  if (Object.hasOwn(patchProperties, key)) {
303
253
  throw new Error(
304
- `${context} resource patch schema field "${key}" cannot use repository.storage "virtual".`
254
+ `${context} resource patch schema field "${key}" cannot use storage.virtual.`
305
255
  );
306
256
  }
307
257
  }
308
258
 
309
259
  const listSearchColumns = [];
310
- for (const [key, schema] of Object.entries(outputProperties)) {
311
- if ((fieldStorageByKey[key] || CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) !== CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) {
260
+ for (const [key, definition] of Object.entries(outputProperties)) {
261
+ if ((fieldStorageByKey[key] || CRUD_FIELD_STORAGE_COLUMN) !== CRUD_FIELD_STORAGE_COLUMN) {
312
262
  continue;
313
263
  }
314
- if (!schemaIncludesStringType(schema)) {
264
+ if (!definitionIncludesStringType(definition)) {
315
265
  continue;
316
266
  }
317
267
 
@@ -324,7 +274,7 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
324
274
 
325
275
  const parentFilterColumns = {};
326
276
  for (const key of resolveCrudLookupFieldKeys(resource, { allowKeys: writeKeys })) {
327
- if ((fieldStorageByKey[key] || CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) !== CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) {
277
+ if ((fieldStorageByKey[key] || CRUD_FIELD_STORAGE_COLUMN) !== CRUD_FIELD_STORAGE_COLUMN) {
328
278
  continue;
329
279
  }
330
280
  const columnName = resolveColumnName(key, columnOverrides);
@@ -335,14 +285,14 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
335
285
  }
336
286
 
337
287
  const outputRecordIdKeys = [];
338
- for (const [key, schema] of Object.entries(outputProperties)) {
339
- if (schemaIncludesRecordIdType(schema)) {
288
+ for (const [key, definition] of Object.entries(outputProperties)) {
289
+ if (definitionIncludesRecordIdType(definition)) {
340
290
  outputRecordIdKeys.push(key);
341
291
  }
342
292
  }
343
293
 
344
294
  for (const key of writeKeys) {
345
- if ((fieldStorageByKey[key] || CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) !== CRUD_FIELD_REPOSITORY_STORAGE_COLUMN) {
295
+ if ((fieldStorageByKey[key] || CRUD_FIELD_STORAGE_COLUMN) !== CRUD_FIELD_STORAGE_COLUMN) {
346
296
  continue;
347
297
  }
348
298
 
@@ -350,12 +300,12 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
350
300
  continue;
351
301
  }
352
302
 
353
- const schema = writeProperties[key] || patchProperties[key];
354
- if (!schemaIncludesDateTimeFormat(schema)) {
303
+ const definition = writeProperties[key] || patchProperties[key];
304
+ if (!definitionIncludesDateTimeType(definition)) {
355
305
  continue;
356
306
  }
357
307
 
358
- writeSerializerByKey[key] = CRUD_FIELD_REPOSITORY_WRITE_SERIALIZER_DATETIME_UTC;
308
+ writeSerializerByKey[key] = CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC;
359
309
  }
360
310
 
361
311
  return Object.freeze({