@kollors/deep-json-server 0.7.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.
- package/README.md +12 -9
- package/README.ru.md +12 -9
- package/dist/bin/deep-json-server.d.ts +2 -0
- package/dist/bin/deep-json-server.js +11 -0
- package/dist/bin/deep-json-server.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.js +80 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +54 -0
- package/dist/src/config.js +145 -0
- package/dist/src/config.js.map +1 -0
- package/{src → dist/src}/constants.js +1 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/database.d.ts +20 -0
- package/dist/src/database.js +136 -0
- package/dist/src/database.js.map +1 -0
- package/dist/src/files/contract.d.ts +67 -0
- package/dist/src/files/contract.js +106 -0
- package/dist/src/files/contract.js.map +1 -0
- package/dist/src/files/disk-store.d.ts +5 -0
- package/dist/src/files/disk-store.js +313 -0
- package/dist/src/files/disk-store.js.map +1 -0
- package/dist/src/files/index.d.ts +5 -0
- package/dist/src/files/index.js +6 -0
- package/dist/src/files/index.js.map +1 -0
- package/dist/src/files/memory-store.d.ts +3 -0
- package/dist/src/files/memory-store.js +80 -0
- package/dist/src/files/memory-store.js.map +1 -0
- package/dist/src/files/routes.d.ts +6 -0
- package/dist/src/files/routes.js +77 -0
- package/dist/src/files/routes.js.map +1 -0
- package/dist/src/openapi/config.d.ts +11 -0
- package/dist/src/openapi/config.js +204 -0
- package/dist/src/openapi/config.js.map +1 -0
- package/dist/src/openapi/document.d.ts +9 -0
- package/dist/src/openapi/document.js +338 -0
- package/dist/src/openapi/document.js.map +1 -0
- package/dist/src/openapi/index.d.ts +10 -0
- package/dist/src/openapi/index.js +27 -0
- package/dist/src/openapi/index.js.map +1 -0
- package/dist/src/openapi/inference.d.ts +14 -0
- package/dist/src/openapi/inference.js +145 -0
- package/dist/src/openapi/inference.js.map +1 -0
- package/dist/src/query/filter.d.ts +6 -0
- package/dist/src/query/filter.js +248 -0
- package/dist/src/query/filter.js.map +1 -0
- package/{src → dist/src}/query/index.js +1 -0
- package/dist/src/query/index.js.map +1 -0
- package/dist/src/query/pagination.d.ts +11 -0
- package/dist/src/query/pagination.js +28 -0
- package/dist/src/query/pagination.js.map +1 -0
- package/dist/src/query/sort.d.ts +1 -0
- package/dist/src/query/sort.js +54 -0
- package/dist/src/query/sort.js.map +1 -0
- package/dist/src/relation-metadata.d.ts +10 -0
- package/dist/src/relation-metadata.js +32 -0
- package/dist/src/relation-metadata.js.map +1 -0
- package/dist/src/relations.d.ts +12 -0
- package/dist/src/relations.js +155 -0
- package/dist/src/relations.js.map +1 -0
- package/dist/src/server.d.ts +13 -0
- package/dist/src/server.js +188 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +63 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +16 -15
- package/bin/deep-json-server.js +0 -11
- package/index.js +0 -12
- package/src/cli.js +0 -91
- package/src/config.js +0 -203
- package/src/database.js +0 -148
- package/src/files.js +0 -633
- package/src/openapi/config.js +0 -124
- package/src/openapi/document.js +0 -394
- package/src/openapi/index.js +0 -36
- package/src/openapi/inference.js +0 -187
- package/src/query/filter.js +0 -283
- package/src/query/pagination.js +0 -44
- package/src/query/sort.js +0 -63
- package/src/relation-metadata.js +0 -41
- package/src/relations.js +0 -173
- package/src/server.js +0 -250
- package/src/utils.js +0 -84
- package/types/index.d.ts +0 -21
- package/types/src/config.d.ts +0 -73
- package/types/src/database.d.ts +0 -33
- package/types/src/files.d.ts +0 -64
- package/types/src/openapi/config.d.ts +0 -3
- package/types/src/openapi/document.d.ts +0 -11
- package/types/src/openapi/index.d.ts +0 -9
- package/types/src/openapi/inference.d.ts +0 -9
- package/types/src/query/filter.d.ts +0 -3
- package/types/src/query/pagination.d.ts +0 -8
- package/types/src/query/sort.d.ts +0 -1
- package/types/src/relation-metadata.d.ts +0 -9
- package/types/src/relations.d.ts +0 -3
- package/types/src/server.d.ts +0 -14
- package/types/src/utils.d.ts +0 -18
- /package/{types → dist}/src/constants.d.ts +0 -0
- /package/{types → dist}/src/query/index.d.ts +0 -0
package/src/openapi/config.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { readJsonObject } from '../database.js';
|
|
2
|
-
import { isObject } from '../utils.js';
|
|
3
|
-
import { applyRequiredFields, getSchemasAtPath, updateSchemasAtPath } from './inference.js';
|
|
4
|
-
|
|
5
|
-
const validateSchemaOverride = (schema, path) => {
|
|
6
|
-
if (!isObject(schema)) {
|
|
7
|
-
throw new Error(`OpenAPI-схема свойства «${path}» должна содержать JSON-объект`);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (schema.properties != null) {
|
|
11
|
-
if (!isObject(schema.properties)) {
|
|
12
|
-
throw new Error(`properties свойства «${path}» должен содержать JSON-объект`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
Object.entries(schema.properties).forEach(([key, value]) => {
|
|
16
|
-
validateSchemaOverride(value, `${path}.${key}`);
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (schema.items != null) {
|
|
21
|
-
validateSchemaOverride(schema.items, `${path}[]`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (schema.oneOf != null) {
|
|
25
|
-
if (!Array.isArray(schema.oneOf) || schema.oneOf.length === 0) {
|
|
26
|
-
throw new Error(`oneOf свойства «${path}» должен содержать непустой массив`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
schema.oneOf.forEach((value, index) => {
|
|
30
|
-
validateSchemaOverride(value, `${path}.oneOf[${index}]`);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const validateSchemaConfig = (schemaConfig, resources) => {
|
|
36
|
-
if (
|
|
37
|
-
Object.hasOwn(schemaConfig, '$info') &&
|
|
38
|
-
(!isObject(schemaConfig.$info) || !['title', 'version'].every((key) => typeof schemaConfig.$info[key] === 'string' && schemaConfig.$info[key].trim() !== ''))
|
|
39
|
-
) {
|
|
40
|
-
throw new Error('$info должен содержать непустые строковые поля title и version');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (Object.hasOwn(schemaConfig, '$schema') && !isObject(schemaConfig.$schema)) {
|
|
44
|
-
throw new Error('$schema должен содержать JSON-объект');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const resourceConfigs = schemaConfig.$schema ?? {};
|
|
48
|
-
|
|
49
|
-
Object.entries(resourceConfigs).forEach(([resource, resourceConfig]) => {
|
|
50
|
-
if (!resources.includes(resource)) {
|
|
51
|
-
throw new Error(`В $schema указан неизвестный ресурс «${resource}»`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!isObject(resourceConfig)) {
|
|
55
|
-
throw new Error(`Настройки ресурса «${resource}» должны содержать JSON-объект`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (resourceConfig.name != null && (typeof resourceConfig.name !== 'string' || resourceConfig.name.trim() === '')) {
|
|
59
|
-
throw new Error(`$schema.${resource}.name должен содержать непустую строку`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (resourceConfig.required != null && (!Array.isArray(resourceConfig.required) || resourceConfig.required.some((path) => typeof path !== 'string' || path === ''))) {
|
|
63
|
-
throw new Error(`$schema.${resource}.required должен содержать массив непустых строк`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
resourceConfig.formats != null &&
|
|
68
|
-
(!isObject(resourceConfig.formats) || Object.entries(resourceConfig.formats).some(([path, format]) => path === '' || typeof format !== 'string' || format === ''))
|
|
69
|
-
) {
|
|
70
|
-
throw new Error(`$schema.${resource}.formats должен содержать JSON-объект с непустыми строковыми путями и форматами`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (resourceConfig.properties != null) {
|
|
74
|
-
if (!isObject(resourceConfig.properties)) {
|
|
75
|
-
throw new Error(`$schema.${resource}.properties должен содержать JSON-объект`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
Object.entries(resourceConfig.properties).forEach(([key, value]) => {
|
|
79
|
-
validateSchemaOverride(value, `${resource}.${key}`);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return resourceConfigs;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export const applyConfiguredFields = (schema, resource, resourceConfig) => {
|
|
88
|
-
const requiredFields = Array.isArray(resourceConfig.required) ? resourceConfig.required : [];
|
|
89
|
-
const formats = isObject(resourceConfig.formats) ? resourceConfig.formats : {};
|
|
90
|
-
|
|
91
|
-
[...requiredFields, ...Object.keys(formats)].forEach((path) => {
|
|
92
|
-
if (getSchemasAtPath(schema, path.split('.')).length === 0) {
|
|
93
|
-
throw new Error(`Путь «${path}» из настроек ресурса «${resource}» отсутствует в итоговой схеме`);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const schemaWithFormats = Object.entries(formats).reduce((result, [path, format]) => {
|
|
98
|
-
const schemas = getSchemasAtPath(result, path.split('.'));
|
|
99
|
-
|
|
100
|
-
if (!schemas.some((nestedSchema) => nestedSchema.type === 'string')) {
|
|
101
|
-
throw new Error(`Формат «${format}» для пути «${resource}.${path}» можно применить только к строковому полю`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return updateSchemasAtPath(result, path.split('.'), (nestedSchema) => (nestedSchema.type === 'string' ? { ...nestedSchema, format } : nestedSchema));
|
|
105
|
-
}, schema);
|
|
106
|
-
|
|
107
|
-
return applyRequiredFields(schemaWithFormats, '', new Set(requiredFields));
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export const resolveSchemaConfig = async (schema) => {
|
|
111
|
-
if (schema == null) {
|
|
112
|
-
return {};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (typeof schema === 'string') {
|
|
116
|
-
return readJsonObject(schema, 'Файл схемы базы данных');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!isObject(schema)) {
|
|
120
|
-
throw new Error('Схема базы данных должна содержать JSON-объект');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return structuredClone(schema);
|
|
124
|
-
};
|
package/src/openapi/document.js
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MAX_PAGE_SIZE, DEFAULT_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
|
-
ContentDirectory: { description: 'URI-encoded relative storage directory', in: 'header', name: 'Content-Directory', schema: { type: 'string' } },
|
|
89
|
-
ContentName: { description: 'URI-encoded file name', in: 'header', name: 'Content-Name', required: true, schema: { type: 'string' } },
|
|
90
|
-
ContentOverride: { description: 'Overwrite an existing file at the same path', in: 'header', name: 'Content-Override', schema: { default: false, type: 'boolean' } },
|
|
91
|
-
Embed: { description: 'Relationship paths to embed', explode: true, in: 'query', name: '_embed', schema: { items: { type: 'string' }, type: 'array' }, style: 'form' },
|
|
92
|
-
FilePath: { allowReserved: true, description: 'URI-encoded file path relative to the storage directory', in: 'path', name: 'path', required: true, schema: { type: 'string' } },
|
|
93
|
-
Id: { in: 'path', name: 'id', required: true, schema: { type: 'string' } },
|
|
94
|
-
Page: { in: 'query', name: '_page', required: false, schema: { default: 1, minimum: 1, type: 'integer' } },
|
|
95
|
-
PerPage: { in: 'query', name: '_perPage', required: false, schema: { default: DEFAULT_PAGE_SIZE, maximum: maxPageSize, minimum: 1, type: 'integer' } },
|
|
96
|
-
Sort: { description: 'Comma-separated fields; prefix with - for descending order', in: 'query', name: '_sort', schema: { type: 'string' } },
|
|
97
|
-
Where: { description: 'JSON-encoded deep filter', in: 'query', name: '_where', schema: { type: 'string' } },
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const createJsonContent = (schema) => ({ content: { 'application/json': { schema } } });
|
|
101
|
-
const createResponse = (description, schema) => ({ description, ...(schema != null && createJsonContent(schema)) });
|
|
102
|
-
const createParameterReference = (name) => ({ $ref: `#/components/parameters/${name}` });
|
|
103
|
-
const createRequestBody = (name) => ({ required: true, ...createJsonContent(createSchemaReference(name)) });
|
|
104
|
-
|
|
105
|
-
const createFilePaths = () => ({
|
|
106
|
-
'/_files/download/{path}': {
|
|
107
|
-
get: {
|
|
108
|
-
operationId: 'downloadFile',
|
|
109
|
-
parameters: [createParameterReference('FilePath')],
|
|
110
|
-
responses: {
|
|
111
|
-
200: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, description: 'File download' },
|
|
112
|
-
400: createResponse('Invalid path', createSchemaReference('Error')),
|
|
113
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
114
|
-
},
|
|
115
|
-
tags: ['files'],
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
'/_files/metadata/{path}': {
|
|
119
|
-
get: {
|
|
120
|
-
operationId: 'getFileMetadata',
|
|
121
|
-
parameters: [createParameterReference('FilePath')],
|
|
122
|
-
responses: {
|
|
123
|
-
200: createResponse('File metadata', createSchemaReference('FileMetadata')),
|
|
124
|
-
400: createResponse('Invalid path', createSchemaReference('Error')),
|
|
125
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
126
|
-
},
|
|
127
|
-
tags: ['files'],
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
'/_files/storage': {
|
|
131
|
-
post: {
|
|
132
|
-
operationId: 'uploadFile',
|
|
133
|
-
parameters: ['ContentName', 'ContentDirectory', 'ContentOverride'].map(createParameterReference),
|
|
134
|
-
requestBody: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, required: true },
|
|
135
|
-
responses: {
|
|
136
|
-
200: createResponse('Overwritten', createSchemaReference('FileMetadata')),
|
|
137
|
-
201: createResponse('Created', createSchemaReference('FileMetadata')),
|
|
138
|
-
400: createResponse('Invalid request', createSchemaReference('Error')),
|
|
139
|
-
409: createResponse('Already exists', createSchemaReference('Error')),
|
|
140
|
-
413: createResponse('File is too large', createSchemaReference('Error')),
|
|
141
|
-
415: createResponse('Unsupported media type', createSchemaReference('Error')),
|
|
142
|
-
},
|
|
143
|
-
tags: ['files'],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
'/_files/storage/{path}': {
|
|
147
|
-
delete: {
|
|
148
|
-
operationId: 'deleteFile',
|
|
149
|
-
parameters: [createParameterReference('FilePath')],
|
|
150
|
-
responses: {
|
|
151
|
-
204: { description: 'Deleted' },
|
|
152
|
-
400: createResponse('Invalid path', createSchemaReference('Error')),
|
|
153
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
154
|
-
},
|
|
155
|
-
tags: ['files'],
|
|
156
|
-
},
|
|
157
|
-
get: {
|
|
158
|
-
operationId: 'getFileContent',
|
|
159
|
-
parameters: [createParameterReference('FilePath')],
|
|
160
|
-
responses: {
|
|
161
|
-
200: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, description: 'File contents' },
|
|
162
|
-
400: createResponse('Invalid path', createSchemaReference('Error')),
|
|
163
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
164
|
-
},
|
|
165
|
-
tags: ['files'],
|
|
166
|
-
},
|
|
167
|
-
patch: {
|
|
168
|
-
operationId: 'updateFile',
|
|
169
|
-
parameters: [createParameterReference('FilePath')],
|
|
170
|
-
requestBody: createRequestBody('FileUpdate'),
|
|
171
|
-
responses: {
|
|
172
|
-
200: createResponse('Updated', createSchemaReference('FileMetadata')),
|
|
173
|
-
400: createResponse('Invalid request', createSchemaReference('Error')),
|
|
174
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
175
|
-
409: createResponse('Already exists', createSchemaReference('Error')),
|
|
176
|
-
413: createResponse('Request is too large', createSchemaReference('Error')),
|
|
177
|
-
415: createResponse('Unsupported media type', createSchemaReference('Error')),
|
|
178
|
-
},
|
|
179
|
-
tags: ['files'],
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const createResourceOperationIds = (resource) => {
|
|
185
|
-
const resourceName = toPascalCase(resource);
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
create: `post${resourceName}`,
|
|
189
|
-
get: `get${resourceName}ById`,
|
|
190
|
-
list: `get${resourceName}`,
|
|
191
|
-
remove: `delete${resourceName}ById`,
|
|
192
|
-
replace: `put${resourceName}ById`,
|
|
193
|
-
update: `patch${resourceName}ById`,
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const createResourcePaths = (resource, componentName) => {
|
|
198
|
-
const operationIds = createResourceOperationIds(resource);
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
[`/${resource}`]: {
|
|
202
|
-
get: {
|
|
203
|
-
operationId: operationIds.list,
|
|
204
|
-
parameters: ['Page', 'PerPage', 'Sort', 'Where', 'Embed'].map(createParameterReference),
|
|
205
|
-
responses: { 200: createResponse('Successful response', createSchemaReference(`${componentName}Page`)), 400: createResponse('Invalid query', createSchemaReference('Error')) },
|
|
206
|
-
tags: [resource],
|
|
207
|
-
},
|
|
208
|
-
post: {
|
|
209
|
-
operationId: operationIds.create,
|
|
210
|
-
requestBody: createRequestBody(`${componentName}Create`),
|
|
211
|
-
responses: { 201: createResponse('Created', createSchemaReference(componentName)), 400: createResponse('Invalid request', createSchemaReference('Error')) },
|
|
212
|
-
tags: [resource],
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
[`/${resource}/{id}`]: {
|
|
216
|
-
delete: {
|
|
217
|
-
operationId: operationIds.remove,
|
|
218
|
-
parameters: [createParameterReference('Id')],
|
|
219
|
-
responses: { 200: createResponse('Deleted', createSchemaReference(componentName)), 404: createResponse('Not found', createSchemaReference('Error')) },
|
|
220
|
-
tags: [resource],
|
|
221
|
-
},
|
|
222
|
-
get: {
|
|
223
|
-
operationId: operationIds.get,
|
|
224
|
-
parameters: [createParameterReference('Id'), createParameterReference('Embed')],
|
|
225
|
-
responses: {
|
|
226
|
-
200: createResponse('Successful response', createSchemaReference(componentName)),
|
|
227
|
-
400: createResponse('Invalid query', createSchemaReference('Error')),
|
|
228
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
229
|
-
},
|
|
230
|
-
tags: [resource],
|
|
231
|
-
},
|
|
232
|
-
patch: {
|
|
233
|
-
operationId: operationIds.update,
|
|
234
|
-
parameters: [createParameterReference('Id')],
|
|
235
|
-
requestBody: createRequestBody(`${componentName}Update`),
|
|
236
|
-
responses: {
|
|
237
|
-
200: createResponse('Updated', createSchemaReference(componentName)),
|
|
238
|
-
400: createResponse('Invalid request', createSchemaReference('Error')),
|
|
239
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
240
|
-
},
|
|
241
|
-
tags: [resource],
|
|
242
|
-
},
|
|
243
|
-
put: {
|
|
244
|
-
operationId: operationIds.replace,
|
|
245
|
-
parameters: [createParameterReference('Id')],
|
|
246
|
-
requestBody: createRequestBody(`${componentName}Create`),
|
|
247
|
-
responses: {
|
|
248
|
-
200: createResponse('Replaced', createSchemaReference(componentName)),
|
|
249
|
-
400: createResponse('Invalid request', createSchemaReference('Error')),
|
|
250
|
-
404: createResponse('Not found', createSchemaReference('Error')),
|
|
251
|
-
},
|
|
252
|
-
tags: [resource],
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const validateGeneratedNames = (resources, componentNames, files) => {
|
|
259
|
-
const schemaOwners = new Map([
|
|
260
|
-
['Error', 'встроенная схема ошибки'],
|
|
261
|
-
...(files
|
|
262
|
-
? [
|
|
263
|
-
['FileMetadata', 'встроенная схема метаданных файла'],
|
|
264
|
-
['FileUpdate', 'встроенная схема изменения файла'],
|
|
265
|
-
]
|
|
266
|
-
: []),
|
|
267
|
-
]);
|
|
268
|
-
const operationOwners = new Map();
|
|
269
|
-
|
|
270
|
-
resources.forEach((resource) => {
|
|
271
|
-
const componentName = componentNames[resource];
|
|
272
|
-
|
|
273
|
-
if (componentName === '') {
|
|
274
|
-
throw new Error(`Не удалось сформировать имя OpenAPI-схемы для ресурса «${resource}». Укажите $schema.${resource}.name`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
[componentName, `${componentName}Create`, `${componentName}Update`, `${componentName}Page`].forEach((schemaName) => {
|
|
278
|
-
const owner = schemaOwners.get(schemaName);
|
|
279
|
-
|
|
280
|
-
if (owner != null) {
|
|
281
|
-
throw new Error(`Имя OpenAPI-схемы «${schemaName}» используется ресурсами «${owner}» и «${resource}». Укажите уникальный $schema.${resource}.name`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
schemaOwners.set(schemaName, resource);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
Object.values(createResourceOperationIds(resource)).forEach((operationId) => {
|
|
288
|
-
const owner = operationOwners.get(operationId);
|
|
289
|
-
|
|
290
|
-
if (owner != null) {
|
|
291
|
-
throw new Error(`operationId «${operationId}» используется ресурсами «${owner}» и «${resource}». Переименуйте один из ресурсов`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
operationOwners.set(operationId, resource);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Builds an OpenAPI document without runtime server addresses.
|
|
301
|
-
* @param {{ database: Record<string, Array<Record<string, unknown>>>, files?: boolean, maxPageSize?: number, schema?: Record<string, unknown> }} options Source data and schema settings.
|
|
302
|
-
* @returns {Record<string, unknown>} OpenAPI document.
|
|
303
|
-
*/
|
|
304
|
-
export function buildOpenapiDocument(options) {
|
|
305
|
-
const { database, files = false, maxPageSize = DEFAULT_MAX_PAGE_SIZE, schema: schemaConfig = {} } = options ?? {};
|
|
306
|
-
|
|
307
|
-
if (typeof files !== 'boolean') {
|
|
308
|
-
throw new Error('Ключ files должен содержать boolean');
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
validateDatabase(database);
|
|
312
|
-
|
|
313
|
-
if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
|
|
314
|
-
throw new Error('Максимальный размер страницы должен быть положительным целым числом');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (!isObject(schemaConfig)) {
|
|
318
|
-
throw new Error('Схема базы данных должна содержать JSON-объект');
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const resources = getResourceNames(database);
|
|
322
|
-
const resourceConfigs = validateSchemaConfig(schemaConfig, resources);
|
|
323
|
-
const componentNames = Object.fromEntries(
|
|
324
|
-
resources.map((resource) => {
|
|
325
|
-
const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
|
|
326
|
-
const componentName = typeof resourceConfig.name === 'string' && resourceConfig.name !== '' ? resourceConfig.name : toPascalCase(singularize(resource));
|
|
327
|
-
|
|
328
|
-
return [resource, componentName];
|
|
329
|
-
}),
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
validateGeneratedNames(resources, componentNames, files);
|
|
333
|
-
|
|
334
|
-
const rawSchemas = Object.fromEntries(
|
|
335
|
-
resources.map((resource) => {
|
|
336
|
-
const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
|
|
337
|
-
const inferredSchema = database[resource].length === 0 ? { properties: { id: { type: 'string' } }, type: 'object' } : inferObjectSchema(database[resource]);
|
|
338
|
-
const configuredSchema = mergeSchemaOverrides(inferredSchema, { properties: isObject(resourceConfig.properties) ? resourceConfig.properties : {} });
|
|
339
|
-
|
|
340
|
-
return [resource, applyConfiguredFields(ensureGeneratedIdSchema(configuredSchema), resource, resourceConfig)];
|
|
341
|
-
}),
|
|
342
|
-
);
|
|
343
|
-
const schemas = {
|
|
344
|
-
Error: { properties: { error: { type: 'string' } }, required: ['error'], type: 'object' },
|
|
345
|
-
...(files && {
|
|
346
|
-
FileMetadata: {
|
|
347
|
-
properties: {
|
|
348
|
-
directory: { type: 'string' },
|
|
349
|
-
downloadUrl: { type: 'string' },
|
|
350
|
-
metadataUrl: { type: 'string' },
|
|
351
|
-
mimeType: { type: 'string' },
|
|
352
|
-
name: { type: 'string' },
|
|
353
|
-
size: { minimum: 0, type: 'integer' },
|
|
354
|
-
url: { type: 'string' },
|
|
355
|
-
},
|
|
356
|
-
required: ['directory', 'downloadUrl', 'metadataUrl', 'mimeType', 'name', 'size', 'url'],
|
|
357
|
-
type: 'object',
|
|
358
|
-
},
|
|
359
|
-
FileUpdate: {
|
|
360
|
-
additionalProperties: false,
|
|
361
|
-
anyOf: [{ required: ['directory'] }, { required: ['name'] }],
|
|
362
|
-
properties: { directory: { type: 'string' }, name: { type: 'string' } },
|
|
363
|
-
type: 'object',
|
|
364
|
-
},
|
|
365
|
-
}),
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
resources.forEach((resource) => {
|
|
369
|
-
const componentName = componentNames[resource];
|
|
370
|
-
const rawSchema = rawSchemas[resource];
|
|
371
|
-
|
|
372
|
-
schemas[componentName] = addForwardRelations(rawSchema, resources, componentNames, resource);
|
|
373
|
-
schemas[`${componentName}Create`] = omitId(rawSchema, true);
|
|
374
|
-
schemas[`${componentName}Update`] = omitId(rawSchema, false);
|
|
375
|
-
schemas[`${componentName}Page`] = {
|
|
376
|
-
properties: {
|
|
377
|
-
data: { items: createSchemaReference(componentName), type: 'array' },
|
|
378
|
-
total: { minimum: 0, type: 'integer' },
|
|
379
|
-
},
|
|
380
|
-
required: ['data', 'total'],
|
|
381
|
-
type: 'object',
|
|
382
|
-
};
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
addReverseRelations(schemas, rawSchemas, resources, componentNames);
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
components: { parameters: createParameters(maxPageSize), schemas },
|
|
389
|
-
info: isObject(schemaConfig.$info) ? schemaConfig.$info : { title: 'Deep JSON Server API', version: '1.0.0' },
|
|
390
|
-
openapi: '3.0.3',
|
|
391
|
-
paths: Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource])), files ? createFilePaths() : {}),
|
|
392
|
-
tags: [...resources.map((resource) => ({ name: resource })), ...(files ? [{ name: 'files' }] : [])],
|
|
393
|
-
};
|
|
394
|
-
}
|
package/src/openapi/index.js
DELETED
|
@@ -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
|
-
};
|