@kollors/deep-json-server 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +52 -20
  2. package/README.ru.md +52 -20
  3. package/dist/bin/deep-json-server.d.ts +2 -0
  4. package/dist/bin/deep-json-server.js +11 -0
  5. package/dist/bin/deep-json-server.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/src/cli.d.ts +4 -0
  10. package/dist/src/cli.js +80 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +54 -0
  13. package/dist/src/config.js +145 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/{types → dist}/src/constants.d.ts +1 -1
  16. package/{src → dist/src}/constants.js +2 -1
  17. package/dist/src/constants.js.map +1 -0
  18. package/dist/src/database.d.ts +20 -0
  19. package/dist/src/database.js +136 -0
  20. package/dist/src/database.js.map +1 -0
  21. package/dist/src/files/contract.d.ts +67 -0
  22. package/dist/src/files/contract.js +106 -0
  23. package/dist/src/files/contract.js.map +1 -0
  24. package/dist/src/files/disk-store.d.ts +5 -0
  25. package/dist/src/files/disk-store.js +313 -0
  26. package/dist/src/files/disk-store.js.map +1 -0
  27. package/dist/src/files/index.d.ts +5 -0
  28. package/dist/src/files/index.js +6 -0
  29. package/dist/src/files/index.js.map +1 -0
  30. package/dist/src/files/memory-store.d.ts +3 -0
  31. package/dist/src/files/memory-store.js +80 -0
  32. package/dist/src/files/memory-store.js.map +1 -0
  33. package/dist/src/files/routes.d.ts +6 -0
  34. package/dist/src/files/routes.js +77 -0
  35. package/dist/src/files/routes.js.map +1 -0
  36. package/dist/src/openapi/config.d.ts +11 -0
  37. package/dist/src/openapi/config.js +204 -0
  38. package/dist/src/openapi/config.js.map +1 -0
  39. package/dist/src/openapi/document.d.ts +9 -0
  40. package/dist/src/openapi/document.js +338 -0
  41. package/dist/src/openapi/document.js.map +1 -0
  42. package/dist/src/openapi/index.d.ts +10 -0
  43. package/dist/src/openapi/index.js +27 -0
  44. package/dist/src/openapi/index.js.map +1 -0
  45. package/dist/src/openapi/inference.d.ts +14 -0
  46. package/dist/src/openapi/inference.js +145 -0
  47. package/dist/src/openapi/inference.js.map +1 -0
  48. package/dist/src/query/filter.d.ts +6 -0
  49. package/dist/src/query/filter.js +248 -0
  50. package/dist/src/query/filter.js.map +1 -0
  51. package/{src → dist/src}/query/index.js +1 -0
  52. package/dist/src/query/index.js.map +1 -0
  53. package/dist/src/query/pagination.d.ts +11 -0
  54. package/dist/src/query/pagination.js +28 -0
  55. package/dist/src/query/pagination.js.map +1 -0
  56. package/dist/src/query/sort.d.ts +1 -0
  57. package/dist/src/query/sort.js +54 -0
  58. package/dist/src/query/sort.js.map +1 -0
  59. package/dist/src/relation-metadata.d.ts +10 -0
  60. package/dist/src/relation-metadata.js +32 -0
  61. package/dist/src/relation-metadata.js.map +1 -0
  62. package/dist/src/relations.d.ts +12 -0
  63. package/dist/src/relations.js +155 -0
  64. package/dist/src/relations.js.map +1 -0
  65. package/dist/src/server.d.ts +13 -0
  66. package/dist/src/server.js +188 -0
  67. package/dist/src/server.js.map +1 -0
  68. package/dist/src/types.d.ts +52 -0
  69. package/dist/src/types.js +2 -0
  70. package/dist/src/types.js.map +1 -0
  71. package/dist/src/utils.d.ts +20 -0
  72. package/dist/src/utils.js +63 -0
  73. package/dist/src/utils.js.map +1 -0
  74. package/package.json +24 -16
  75. package/bin/deep-json-server.js +0 -11
  76. package/index.js +0 -7
  77. package/src/cli.js +0 -91
  78. package/src/config.js +0 -194
  79. package/src/database.js +0 -157
  80. package/src/files.js +0 -368
  81. package/src/openapi/config.js +0 -124
  82. package/src/openapi/document.js +0 -317
  83. package/src/openapi/index.js +0 -36
  84. package/src/openapi/inference.js +0 -187
  85. package/src/query/filter.js +0 -282
  86. package/src/query/pagination.js +0 -44
  87. package/src/query/sort.js +0 -63
  88. package/src/relation-metadata.js +0 -40
  89. package/src/relations.js +0 -173
  90. package/src/server.js +0 -248
  91. package/src/utils.js +0 -54
  92. package/types/index.d.ts +0 -11
  93. package/types/src/config.d.ts +0 -86
  94. package/types/src/database.d.ts +0 -11
  95. package/types/src/files.d.ts +0 -24
  96. package/types/src/openapi/config.d.ts +0 -3
  97. package/types/src/openapi/document.d.ts +0 -11
  98. package/types/src/openapi/index.d.ts +0 -9
  99. package/types/src/openapi/inference.d.ts +0 -9
  100. package/types/src/query/filter.d.ts +0 -3
  101. package/types/src/query/pagination.d.ts +0 -8
  102. package/types/src/query/sort.d.ts +0 -1
  103. package/types/src/relation-metadata.d.ts +0 -9
  104. package/types/src/relations.d.ts +0 -3
  105. package/types/src/server.d.ts +0 -13
  106. package/types/src/utils.d.ts +0 -10
  107. /package/{types → dist}/src/query/index.d.ts +0 -0
@@ -1,317 +0,0 @@
1
- import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../constants.js';
2
- import { validateDatabase } from '../database.js';
3
- import { getRelationMetadata } from '../relation-metadata.js';
4
- import { getResourceNames, isObject, singularize, toPascalCase } from '../utils.js';
5
- import { applyConfiguredFields, validateSchemaConfig } from './config.js';
6
- import { ensureGeneratedIdSchema, inferObjectSchema, mergeSchemaOverrides, omitId } from './inference.js';
7
-
8
- const createSchemaReference = (name) => ({ $ref: `#/components/schemas/${name}` });
9
-
10
- const addForwardRelations = (schema, resources, componentNames, sourceResource) => {
11
- if (Array.isArray(schema.oneOf)) {
12
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => addForwardRelations(nestedSchema, resources, componentNames, sourceResource)) };
13
- }
14
-
15
- if (schema.type === 'array') {
16
- return { ...schema, items: addForwardRelations(schema.items ?? {}, resources, componentNames, sourceResource) };
17
- }
18
-
19
- if (schema.type !== 'object' || !isObject(schema.properties)) {
20
- return schema;
21
- }
22
-
23
- const properties = Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, addForwardRelations(value, resources, componentNames, sourceResource)]));
24
-
25
- Object.keys(schema.properties).forEach((key) => {
26
- const relation = getRelationMetadata(key, resources, sourceResource);
27
-
28
- if (relation != null) {
29
- const relationSchema = createSchemaReference(componentNames[relation.targetResource]);
30
-
31
- properties[relation.relationName] = relation.isMany ? { items: relationSchema, type: 'array' } : relationSchema;
32
- }
33
- });
34
-
35
- return { ...schema, properties };
36
- };
37
-
38
- const collectRelations = (schema, resources, sourceResource, relations = []) => {
39
- if (Array.isArray(schema.oneOf)) {
40
- schema.oneOf.forEach((nestedSchema) => {
41
- collectRelations(nestedSchema, resources, sourceResource, relations);
42
- });
43
- return relations;
44
- }
45
-
46
- if (schema.type === 'array') {
47
- collectRelations(schema.items ?? {}, resources, sourceResource, relations);
48
- return relations;
49
- }
50
-
51
- if (schema.type !== 'object' || !isObject(schema.properties)) {
52
- return relations;
53
- }
54
-
55
- Object.entries(schema.properties).forEach(([key, value]) => {
56
- const relation = getRelationMetadata(key, resources, sourceResource);
57
-
58
- if (relation != null) {
59
- relations.push(relation);
60
- }
61
-
62
- collectRelations(value, resources, sourceResource, relations);
63
- });
64
-
65
- return relations;
66
- };
67
-
68
- const addReverseRelations = (schemas, rawSchemas, resources, componentNames) => {
69
- resources.forEach((sourceResource) => {
70
- collectRelations(rawSchemas[sourceResource], resources, sourceResource).forEach(({ reverseRelationName, targetResource }) => {
71
- const targetComponentName = componentNames[targetResource];
72
- const targetSchema = schemas[targetComponentName];
73
-
74
- if (targetSchema.type === 'object' && isObject(targetSchema.properties) && !Object.hasOwn(targetSchema.properties, reverseRelationName)) {
75
- schemas[targetComponentName] = {
76
- ...targetSchema,
77
- properties: {
78
- ...targetSchema.properties,
79
- [reverseRelationName]: { items: createSchemaReference(componentNames[sourceResource]), type: 'array' },
80
- },
81
- };
82
- }
83
- });
84
- });
85
- };
86
-
87
- const createParameters = (maxPageSize) => ({
88
- ContentName: { description: 'URI-encoded relative file name', in: 'header', name: 'Content-Name', required: true, schema: { type: 'string' } },
89
- Embed: { description: 'Relationship paths to embed', explode: true, in: 'query', name: '_embed', schema: { items: { type: 'string' }, type: 'array' }, style: 'form' },
90
- Id: { in: 'path', name: 'id', required: true, schema: { type: 'string' } },
91
- Page: { in: 'query', name: '_page', required: false, schema: { default: 1, minimum: 1, type: 'integer' } },
92
- PerPage: { in: 'query', name: '_perPage', required: false, schema: { default: DEFAULT_PAGE_SIZE, maximum: maxPageSize, minimum: 1, type: 'integer' } },
93
- Sort: { description: 'Comma-separated fields; prefix with - for descending order', in: 'query', name: '_sort', schema: { type: 'string' } },
94
- Where: { description: 'JSON-encoded deep filter', in: 'query', name: '_where', schema: { type: 'string' } },
95
- });
96
-
97
- const createJsonContent = (schema) => ({ content: { 'application/json': { schema } } });
98
- const createResponse = (description, schema) => ({ description, ...(schema != null && createJsonContent(schema)) });
99
- const createParameterReference = (name) => ({ $ref: `#/components/parameters/${name}` });
100
- const createRequestBody = (name) => ({ required: true, ...createJsonContent(createSchemaReference(name)) });
101
-
102
- const createFilePaths = () => ({
103
- '/_files': {
104
- post: {
105
- operationId: 'uploadFile',
106
- parameters: [createParameterReference('ContentName')],
107
- requestBody: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, required: true },
108
- responses: {
109
- 201: createResponse('Uploaded', createSchemaReference('UploadedFile')),
110
- 400: createResponse('Invalid request', createSchemaReference('Error')),
111
- 413: createResponse('File is too large', createSchemaReference('Error')),
112
- 415: createResponse('Unsupported media type', createSchemaReference('Error')),
113
- },
114
- tags: ['files'],
115
- },
116
- },
117
- '/_files/{id}': {
118
- delete: {
119
- operationId: 'deleteFileById',
120
- parameters: [createParameterReference('Id')],
121
- responses: { 200: createResponse('Deleted', createSchemaReference('UploadedFile')), 404: createResponse('Not found', createSchemaReference('Error')) },
122
- tags: ['files'],
123
- },
124
- get: {
125
- operationId: 'getFileById',
126
- parameters: [createParameterReference('Id')],
127
- responses: {
128
- 200: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, description: 'File contents' },
129
- 404: createResponse('Not found', createSchemaReference('Error')),
130
- },
131
- tags: ['files'],
132
- },
133
- },
134
- });
135
-
136
- const createResourcePaths = (resource, componentName) => {
137
- const resourceName = toPascalCase(resource);
138
-
139
- return {
140
- [`/${resource}`]: {
141
- get: {
142
- operationId: `get${resourceName}`,
143
- parameters: ['Page', 'PerPage', 'Sort', 'Where', 'Embed'].map(createParameterReference),
144
- responses: { 200: createResponse('Successful response', createSchemaReference(`${componentName}Page`)), 400: createResponse('Invalid query', createSchemaReference('Error')) },
145
- tags: [resource],
146
- },
147
- post: {
148
- operationId: `post${resourceName}`,
149
- requestBody: createRequestBody(`${componentName}Create`),
150
- responses: { 201: createResponse('Created', createSchemaReference(componentName)), 400: createResponse('Invalid request', createSchemaReference('Error')) },
151
- tags: [resource],
152
- },
153
- },
154
- [`/${resource}/{id}`]: {
155
- delete: {
156
- operationId: `delete${resourceName}ById`,
157
- parameters: [createParameterReference('Id')],
158
- responses: { 200: createResponse('Deleted', createSchemaReference(componentName)), 404: createResponse('Not found', createSchemaReference('Error')) },
159
- tags: [resource],
160
- },
161
- get: {
162
- operationId: `get${resourceName}ById`,
163
- parameters: [createParameterReference('Id'), createParameterReference('Embed')],
164
- responses: {
165
- 200: createResponse('Successful response', createSchemaReference(componentName)),
166
- 400: createResponse('Invalid query', createSchemaReference('Error')),
167
- 404: createResponse('Not found', createSchemaReference('Error')),
168
- },
169
- tags: [resource],
170
- },
171
- patch: {
172
- operationId: `patch${resourceName}ById`,
173
- parameters: [createParameterReference('Id')],
174
- requestBody: createRequestBody(`${componentName}Update`),
175
- responses: {
176
- 200: createResponse('Updated', createSchemaReference(componentName)),
177
- 400: createResponse('Invalid request', createSchemaReference('Error')),
178
- 404: createResponse('Not found', createSchemaReference('Error')),
179
- },
180
- tags: [resource],
181
- },
182
- put: {
183
- operationId: `put${resourceName}ById`,
184
- parameters: [createParameterReference('Id')],
185
- requestBody: createRequestBody(`${componentName}Create`),
186
- responses: {
187
- 200: createResponse('Replaced', createSchemaReference(componentName)),
188
- 400: createResponse('Invalid request', createSchemaReference('Error')),
189
- 404: createResponse('Not found', createSchemaReference('Error')),
190
- },
191
- tags: [resource],
192
- },
193
- },
194
- };
195
- };
196
-
197
- const getOperationIds = (resource) => {
198
- const resourceName = toPascalCase(resource);
199
-
200
- return [`get${resourceName}`, `post${resourceName}`, `delete${resourceName}ById`, `get${resourceName}ById`, `patch${resourceName}ById`, `put${resourceName}ById`];
201
- };
202
-
203
- const validateGeneratedNames = (resources, componentNames, files) => {
204
- const schemaOwners = new Map([['Error', 'встроенная схема ошибки'], ...(files ? [['UploadedFile', 'встроенная схема файла']] : [])]);
205
- const operationOwners = new Map();
206
-
207
- resources.forEach((resource) => {
208
- const componentName = componentNames[resource];
209
-
210
- if (componentName === '') {
211
- throw new Error(`Не удалось сформировать имя OpenAPI-схемы для ресурса «${resource}». Укажите $schema.${resource}.name`);
212
- }
213
-
214
- [componentName, `${componentName}Create`, `${componentName}Update`, `${componentName}Page`].forEach((schemaName) => {
215
- const owner = schemaOwners.get(schemaName);
216
-
217
- if (owner != null) {
218
- throw new Error(`Имя OpenAPI-схемы «${schemaName}» используется ресурсами «${owner}» и «${resource}». Укажите уникальный $schema.${resource}.name`);
219
- }
220
-
221
- schemaOwners.set(schemaName, resource);
222
- });
223
-
224
- getOperationIds(resource).forEach((operationId) => {
225
- const owner = operationOwners.get(operationId);
226
-
227
- if (owner != null) {
228
- throw new Error(`operationId «${operationId}» используется ресурсами «${owner}» и «${resource}». Переименуйте один из ресурсов`);
229
- }
230
-
231
- operationOwners.set(operationId, resource);
232
- });
233
- });
234
- };
235
-
236
- /**
237
- * Builds an OpenAPI 3.0 document from resolved server data.
238
- * @param {{ database: Record<string, Array<Record<string, unknown>>>, files?: boolean, maxPageSize?: number, schema?: Record<string, unknown> }} options Document options.
239
- * @returns {Record<string, unknown>} OpenAPI document.
240
- */
241
- export function buildOpenapiDocument(options) {
242
- const { database, files = false, maxPageSize = MAX_PAGE_SIZE, schema: schemaConfig = {} } = options ?? {};
243
-
244
- if (typeof files !== 'boolean') {
245
- throw new Error('Ключ files должен содержать boolean');
246
- }
247
-
248
- validateDatabase(database);
249
-
250
- if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
251
- throw new Error('Максимальный размер страницы должен быть положительным целым числом');
252
- }
253
-
254
- if (!isObject(schemaConfig)) {
255
- throw new Error('Схема базы данных должна содержать JSON-объект');
256
- }
257
-
258
- const resources = getResourceNames(database);
259
- const resourceConfigs = validateSchemaConfig(schemaConfig, resources);
260
- const componentNames = Object.fromEntries(
261
- resources.map((resource) => {
262
- const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
263
- const componentName = typeof resourceConfig.name === 'string' && resourceConfig.name !== '' ? resourceConfig.name : toPascalCase(singularize(resource));
264
-
265
- return [resource, componentName];
266
- }),
267
- );
268
-
269
- validateGeneratedNames(resources, componentNames, files);
270
-
271
- const rawSchemas = Object.fromEntries(
272
- resources.map((resource) => {
273
- const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
274
- const inferredSchema = database[resource].length === 0 ? { properties: { id: { type: 'string' } }, type: 'object' } : inferObjectSchema(database[resource]);
275
- const configuredSchema = mergeSchemaOverrides(inferredSchema, { properties: isObject(resourceConfig.properties) ? resourceConfig.properties : {} });
276
-
277
- return [resource, applyConfiguredFields(ensureGeneratedIdSchema(configuredSchema), resource, resourceConfig)];
278
- }),
279
- );
280
- const schemas = {
281
- Error: { properties: { error: { type: 'string' } }, required: ['error'], type: 'object' },
282
- ...(files && {
283
- UploadedFile: {
284
- properties: { id: { type: 'string' }, mimeType: { type: 'string' }, name: { type: 'string' }, size: { minimum: 0, type: 'integer' }, url: { type: 'string' } },
285
- required: ['id', 'mimeType', 'name', 'size', 'url'],
286
- type: 'object',
287
- },
288
- }),
289
- };
290
-
291
- resources.forEach((resource) => {
292
- const componentName = componentNames[resource];
293
- const rawSchema = rawSchemas[resource];
294
-
295
- schemas[componentName] = addForwardRelations(rawSchema, resources, componentNames, resource);
296
- schemas[`${componentName}Create`] = omitId(rawSchema, true);
297
- schemas[`${componentName}Update`] = omitId(rawSchema, false);
298
- schemas[`${componentName}Page`] = {
299
- properties: {
300
- data: { items: createSchemaReference(componentName), type: 'array' },
301
- total: { minimum: 0, type: 'integer' },
302
- },
303
- required: ['data', 'total'],
304
- type: 'object',
305
- };
306
- });
307
-
308
- addReverseRelations(schemas, rawSchemas, resources, componentNames);
309
-
310
- return {
311
- components: { parameters: createParameters(maxPageSize), schemas },
312
- info: isObject(schemaConfig.$info) ? schemaConfig.$info : { title: 'Deep JSON Server API', version: '1.0.0' },
313
- openapi: '3.0.3',
314
- paths: Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource])), files ? createFilePaths() : {}),
315
- tags: [...resources.map((resource) => ({ name: resource })), ...(files ? [{ name: 'files' }] : [])],
316
- };
317
- }
@@ -1,36 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { dirname, resolve } from 'node:path';
3
- import { stringify } from 'yaml';
4
- import { DEFAULT_HOST, DEFAULT_PORT } from '../constants.js';
5
- import { buildOpenapiDocument } from './document.js';
6
-
7
- const getServerUrl = (host, port) => {
8
- const serverPort = Number(port);
9
-
10
- if (typeof host !== 'string' || host === '') {
11
- throw new Error('Адрес сервера не должен быть пустым');
12
- }
13
-
14
- if (!Number.isInteger(serverPort) || serverPort < 1 || serverPort > 65_535) {
15
- throw new Error('Порт должен быть целым числом от 1 до 65535');
16
- }
17
-
18
- const serverHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
19
-
20
- return `http://${serverHost}:${serverPort}`;
21
- };
22
-
23
- export const createOpenapi = ({ database, files, host = DEFAULT_HOST, maxPageSize, port = DEFAULT_PORT, schema }) => {
24
- const document = buildOpenapiDocument({ database, files, maxPageSize, schema });
25
-
26
- document.servers = [{ url: getServerUrl(host, port) }];
27
-
28
- return document;
29
- };
30
-
31
- export const writeOpenapi = async (document, outputPath) => {
32
- const resolvedOutputPath = resolve(outputPath);
33
-
34
- await mkdir(dirname(resolvedOutputPath), { recursive: true });
35
- await writeFile(resolvedOutputPath, stringify(document, { aliasDuplicateObjects: false, lineWidth: 0 }), 'utf8');
36
- };
@@ -1,187 +0,0 @@
1
- import { isObject } from '../utils.js';
2
-
3
- export const mergeSchemas = (schemas) => {
4
- const uniqueSchemas = [...new Map(schemas.map((schema) => [JSON.stringify(schema), schema])).values()];
5
- const nullable = uniqueSchemas.some((schema) => schema.type === 'null');
6
- const nonNullSchemas = uniqueSchemas.filter((schema) => schema.type !== 'null');
7
-
8
- if (nonNullSchemas.length === 0) {
9
- return { nullable: true };
10
- }
11
-
12
- if (nonNullSchemas.length === 1) {
13
- return nullable ? { ...nonNullSchemas[0], nullable: true } : nonNullSchemas[0];
14
- }
15
-
16
- return { oneOf: nonNullSchemas, ...(nullable && { nullable: true }) };
17
- };
18
-
19
- export const mergeSchemaOverrides = (schema, overrides) => {
20
- if (!isObject(overrides)) {
21
- return schema;
22
- }
23
-
24
- const result = { ...schema, ...overrides };
25
-
26
- if (Object.hasOwn(overrides, 'type') && !Object.hasOwn(overrides, 'oneOf')) {
27
- delete result.oneOf;
28
- }
29
-
30
- if (isObject(schema.properties) || isObject(overrides.properties)) {
31
- const properties = isObject(schema.properties) ? { ...schema.properties } : {};
32
-
33
- Object.entries(isObject(overrides.properties) ? overrides.properties : {}).forEach(([key, value]) => {
34
- properties[key] = mergeSchemaOverrides(properties[key] ?? {}, value);
35
- });
36
-
37
- result.properties = properties;
38
- }
39
-
40
- if (isObject(schema.items) && isObject(overrides.items)) {
41
- result.items = mergeSchemaOverrides(schema.items, overrides.items);
42
- }
43
-
44
- return result;
45
- };
46
-
47
- export const applyRequiredFields = (schema, path, requiredFields) => {
48
- if (Array.isArray(schema.oneOf)) {
49
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => applyRequiredFields(nestedSchema, path, requiredFields)) };
50
- }
51
-
52
- if (schema.type === 'array') {
53
- return { ...schema, items: applyRequiredFields(schema.items ?? {}, path, requiredFields) };
54
- }
55
-
56
- if (schema.type !== 'object' || !isObject(schema.properties)) {
57
- return schema;
58
- }
59
-
60
- const properties = Object.fromEntries(
61
- Object.entries(schema.properties).map(([key, value]) => {
62
- const fieldPath = path === '' ? key : `${path}.${key}`;
63
-
64
- return [key, applyRequiredFields(value, fieldPath, requiredFields)];
65
- }),
66
- );
67
- const required = Object.keys(properties).filter((key) => {
68
- const fieldPath = path === '' ? key : `${path}.${key}`;
69
-
70
- return (path === '' && key === 'id') || requiredFields.has(fieldPath);
71
- });
72
- const result = { ...schema, properties };
73
-
74
- delete result.required;
75
-
76
- return required.length === 0 ? result : { ...result, required };
77
- };
78
-
79
- export const inferSchema = (values) => {
80
- const schemas = [];
81
- const arrays = values.filter(Array.isArray);
82
- const objects = values.filter(isObject);
83
-
84
- if (arrays.length > 0) {
85
- const items = arrays.flat();
86
-
87
- schemas.push({ items: items.length === 0 ? {} : inferSchema(items), type: 'array' });
88
- }
89
-
90
- if (objects.length > 0) {
91
- schemas.push(inferObjectSchema(objects));
92
- }
93
-
94
- values
95
- .filter((value) => !Array.isArray(value) && !isObject(value))
96
- .forEach((value) => {
97
- if (value === null) {
98
- schemas.push({ type: 'null' });
99
- } else if (typeof value === 'number') {
100
- schemas.push({ type: Number.isInteger(value) ? 'integer' : 'number' });
101
- } else {
102
- schemas.push({ type: typeof value });
103
- }
104
- });
105
-
106
- return mergeSchemas(schemas);
107
- };
108
-
109
- export function inferObjectSchema(values) {
110
- const keys = [...new Set(values.flatMap((value) => Object.keys(value)))].sort((left, right) => (left === 'id' ? -1 : right === 'id' ? 1 : left.localeCompare(right)));
111
- const properties = Object.fromEntries(
112
- keys.map((key) => {
113
- const fieldValues = values.filter((value) => Object.hasOwn(value, key)).map((value) => value[key]);
114
-
115
- return [key, inferSchema(fieldValues)];
116
- }),
117
- );
118
-
119
- return { properties, type: 'object' };
120
- }
121
-
122
- export const ensureGeneratedIdSchema = (schema) => {
123
- const idSchema = schema.properties?.id ?? {};
124
- const idSchemas = Array.isArray(idSchema.oneOf) ? idSchema.oneOf : [idSchema];
125
- const properties = {
126
- ...schema.properties,
127
- id: idSchemas.some(({ type }) => type === 'string') ? idSchema : { oneOf: [...idSchemas, { type: 'string' }] },
128
- };
129
-
130
- return { ...schema, properties };
131
- };
132
-
133
- export const getSchemasAtPath = (schema, keys) => {
134
- if (Array.isArray(schema.oneOf)) {
135
- return schema.oneOf.flatMap((nestedSchema) => getSchemasAtPath(nestedSchema, keys));
136
- }
137
-
138
- if (schema.type === 'array') {
139
- return getSchemasAtPath(schema.items ?? {}, keys);
140
- }
141
-
142
- if (keys.length === 0) {
143
- return [schema];
144
- }
145
-
146
- if (schema.type !== 'object' || !isObject(schema.properties) || !Object.hasOwn(schema.properties, keys[0])) {
147
- return [];
148
- }
149
-
150
- return getSchemasAtPath(schema.properties[keys[0]], keys.slice(1));
151
- };
152
-
153
- export const updateSchemasAtPath = (schema, keys, update) => {
154
- if (Array.isArray(schema.oneOf)) {
155
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => updateSchemasAtPath(nestedSchema, keys, update)) };
156
- }
157
-
158
- if (schema.type === 'array') {
159
- return { ...schema, items: updateSchemasAtPath(schema.items ?? {}, keys, update) };
160
- }
161
-
162
- if (keys.length === 0) {
163
- return update(schema);
164
- }
165
-
166
- if (schema.type !== 'object' || !isObject(schema.properties) || !Object.hasOwn(schema.properties, keys[0])) {
167
- return schema;
168
- }
169
-
170
- return {
171
- ...schema,
172
- properties: {
173
- ...schema.properties,
174
- [keys[0]]: updateSchemasAtPath(schema.properties[keys[0]], keys.slice(1), update),
175
- },
176
- };
177
- };
178
-
179
- export const omitId = (schema, keepRequired) => {
180
- const properties = Object.fromEntries(Object.entries(schema.properties ?? {}).filter(([key]) => key !== 'id'));
181
- const required = keepRequired ? schema.required?.filter((key) => key !== 'id') : undefined;
182
- const result = { ...schema, properties };
183
-
184
- delete result.required;
185
-
186
- return required?.length > 0 ? { ...result, required } : result;
187
- };