@kollors/deep-json-server 0.9.0 → 1.0.0-alpha.2
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 +313 -403
- package/README.ru.md +307 -397
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/cli.js +95 -66
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +18 -2
- package/dist/src/config.js +39 -10
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +2 -1
- package/dist/src/constants.js +2 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/database.d.ts +4 -3
- package/dist/src/database.js +32 -19
- package/dist/src/database.js.map +1 -1
- package/dist/src/engine.d.ts +47 -0
- package/dist/src/engine.js +371 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/features.d.ts +8 -0
- package/dist/src/features.js +18 -0
- package/dist/src/features.js.map +1 -0
- package/dist/src/files/disk-store.js +17 -6
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/files/openapi.d.ts +3 -0
- package/dist/src/files/openapi.js +86 -0
- package/dist/src/files/openapi.js.map +1 -0
- package/dist/src/graphql/preflight.d.ts +3 -0
- package/dist/src/graphql/preflight.js +28 -0
- package/dist/src/graphql/preflight.js.map +1 -0
- package/dist/src/graphql/routes.d.ts +4 -0
- package/dist/src/graphql/routes.js +28 -0
- package/dist/src/graphql/routes.js.map +1 -0
- package/dist/src/graphql.d.ts +4 -0
- package/dist/src/graphql.js +227 -0
- package/dist/src/graphql.js.map +1 -0
- package/dist/src/model.d.ts +65 -0
- package/dist/src/model.js +345 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/openapi/document.d.ts +7 -7
- package/dist/src/openapi/document.js +237 -320
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/index.d.ts +3 -6
- package/dist/src/openapi/index.js +2 -3
- package/dist/src/openapi/index.js.map +1 -1
- package/dist/src/query/contract.d.ts +3 -0
- package/dist/src/query/contract.js +19 -0
- package/dist/src/query/contract.js.map +1 -0
- package/dist/src/query/filter.d.ts +3 -5
- package/dist/src/query/filter.js +76 -240
- package/dist/src/query/filter.js.map +1 -1
- package/dist/src/query/options.d.ts +18 -0
- package/dist/src/query/options.js +30 -0
- package/dist/src/query/options.js.map +1 -0
- package/dist/src/rest/options.d.ts +14 -0
- package/dist/src/rest/options.js +108 -0
- package/dist/src/rest/options.js.map +1 -0
- package/dist/src/rest/projection.d.ts +6 -0
- package/dist/src/rest/projection.js +37 -0
- package/dist/src/rest/projection.js.map +1 -0
- package/dist/src/rest/routes.d.ts +3 -0
- package/dist/src/rest/routes.js +48 -0
- package/dist/src/rest/routes.js.map +1 -0
- package/dist/src/schema.d.ts +17 -0
- package/dist/src/schema.js +23 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/server.d.ts +3 -5
- package/dist/src/server.js +76 -162
- package/dist/src/server.js.map +1 -1
- package/dist/src/types.d.ts +1 -3
- package/dist/src/utils.d.ts +0 -1
- package/dist/src/utils.js +0 -1
- package/dist/src/utils.js.map +1 -1
- package/package.json +9 -4
- package/dist/src/openapi/config.d.ts +0 -11
- package/dist/src/openapi/config.js +0 -204
- package/dist/src/openapi/config.js.map +0 -1
- package/dist/src/openapi/inference.d.ts +0 -14
- package/dist/src/openapi/inference.js +0 -145
- package/dist/src/openapi/inference.js.map +0 -1
- package/dist/src/query/index.d.ts +0 -3
- package/dist/src/query/index.js +0 -4
- package/dist/src/query/index.js.map +0 -1
- package/dist/src/query/pagination.d.ts +0 -11
- package/dist/src/query/pagination.js +0 -28
- package/dist/src/query/pagination.js.map +0 -1
- package/dist/src/query/sort.d.ts +0 -1
- package/dist/src/query/sort.js +0 -54
- package/dist/src/query/sort.js.map +0 -1
- package/dist/src/relations.d.ts +0 -12
- package/dist/src/relations.js +0 -155
- package/dist/src/relations.js.map +0 -1
|
@@ -1,338 +1,255 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { VERSION } from '../constants.js';
|
|
2
|
+
import { FILE_HEADERS, FILE_METADATA_SCHEMA, FILE_UPDATE_SCHEMA } from '../files/contract.js';
|
|
3
|
+
import { createFilePaths } from '../files/openapi.js';
|
|
4
|
+
import { assertApi, nodeName, objectSchema, operationName, valueSchema } from '../model.js';
|
|
5
|
+
import { operatorsFor } from '../query/contract.js';
|
|
6
|
+
import { childrenOf, sortableFields } from '../query/options.js';
|
|
7
|
+
import { isObject } from '../utils.js';
|
|
8
|
+
const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
9
|
+
const json = (schema) => ({ content: { 'application/json': { schema } } });
|
|
10
|
+
const response = (description, schema) => ({ description, ...json(schema) });
|
|
11
|
+
function toOpenapi(schema) {
|
|
12
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.some((v) => isObject(v) && v.type === 'null')) {
|
|
13
|
+
const nonNull = schema.anyOf.find((v) => isObject(v) && v.type !== 'null');
|
|
14
|
+
const converted = toOpenapi(nonNull);
|
|
15
|
+
return { ...converted, nullable: true, ...(converted.enum ? { enum: [...converted.enum.filter((value) => value !== null), null] } : {}) };
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const result = { ...schema };
|
|
18
|
+
if (isObject(schema.properties))
|
|
19
|
+
result.properties = Object.fromEntries(Object.entries(schema.properties).map(([k, v]) => [k, toOpenapi(v)]));
|
|
20
|
+
if (isObject(schema.items))
|
|
21
|
+
result.items = toOpenapi(schema.items);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
export function buildOpenapiDocument({ model, files = false, pageSize = 10, maxPageSize = 100, info = { title: 'Deep JSON Server API', version: VERSION }, }) {
|
|
25
|
+
assertApi(model, 'openapi');
|
|
26
|
+
const schemas = {
|
|
27
|
+
Error: { type: 'object', properties: { error: { type: 'string' } }, required: ['error'] },
|
|
28
|
+
Pager: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: { page: { type: 'integer', minimum: 1, default: 1 }, pageSize: { type: 'integer', minimum: 1, maximum: maxPageSize, default: pageSize } },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const owners = new Map();
|
|
35
|
+
function reserve(name, node) {
|
|
36
|
+
if (owners.get(name) === node)
|
|
37
|
+
return false;
|
|
38
|
+
if (schemas[name])
|
|
39
|
+
throw new Error(`OpenAPI schema name collision: ${name}`);
|
|
40
|
+
owners.set(name, node);
|
|
41
|
+
schemas[name] = {};
|
|
42
|
+
return true;
|
|
15
43
|
}
|
|
16
|
-
|
|
44
|
+
function baseField(node) {
|
|
45
|
+
const schema = toOpenapi(valueSchema(node));
|
|
46
|
+
for (const key of ['description', 'example', 'default', 'readOnly', 'writeOnly'])
|
|
47
|
+
if (node[key] !== undefined)
|
|
48
|
+
schema[key] = node[key];
|
|
49
|
+
if (node.generated)
|
|
50
|
+
schema.readOnly = true;
|
|
17
51
|
return schema;
|
|
18
52
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
53
|
+
function annotateInput(schema, node, defaults) {
|
|
54
|
+
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
55
|
+
const child = node.children[key];
|
|
56
|
+
for (const attribute of ['description', 'example', 'writeOnly'])
|
|
57
|
+
if (child[attribute] !== undefined)
|
|
58
|
+
property[attribute] = child[attribute];
|
|
59
|
+
if (defaults && child.default !== undefined)
|
|
60
|
+
property.default = child.default;
|
|
61
|
+
if (child.base === 'object')
|
|
62
|
+
annotateInput(child.many ? property.items : property, child, defaults);
|
|
25
63
|
}
|
|
26
|
-
|
|
27
|
-
return { ...schema, properties };
|
|
28
|
-
};
|
|
29
|
-
const collectRelations = (schema, resources, sourceResource, relations = []) => {
|
|
30
|
-
if (Array.isArray(schema.oneOf)) {
|
|
31
|
-
schema.oneOf.forEach((nestedSchema) => {
|
|
32
|
-
collectRelations(nestedSchema, resources, sourceResource, relations);
|
|
33
|
-
});
|
|
34
|
-
return relations;
|
|
64
|
+
return schema;
|
|
35
65
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
function output(entity, node) {
|
|
67
|
+
if (node.relation) {
|
|
68
|
+
entity = node.relation;
|
|
69
|
+
node = entity.root;
|
|
70
|
+
}
|
|
71
|
+
const name = nodeName(entity, node);
|
|
72
|
+
if (!reserve(name, node))
|
|
73
|
+
return ref(name);
|
|
74
|
+
const properties = {};
|
|
75
|
+
for (const [key, child] of Object.entries(node.children)) {
|
|
76
|
+
if (child.writeOnly)
|
|
77
|
+
continue;
|
|
78
|
+
if (child.relation || child.base === 'object') {
|
|
79
|
+
const value = child.many ? page(entity, child) : output(entity, child);
|
|
80
|
+
properties[key] = child.nullable || (child.relation && !child.many && !child.required) ? { anyOf: [value, { type: 'object', nullable: true, enum: [null] }] } : value;
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
properties[key] = baseField(child);
|
|
84
|
+
}
|
|
85
|
+
// scope may select any subset, so response properties are intentionally optional.
|
|
86
|
+
schemas[name] = { type: 'object', additionalProperties: false, properties };
|
|
87
|
+
return ref(name);
|
|
39
88
|
}
|
|
40
|
-
|
|
41
|
-
|
|
89
|
+
function page(entity, node) {
|
|
90
|
+
if (node.relation) {
|
|
91
|
+
entity = node.relation;
|
|
92
|
+
node = entity.root;
|
|
93
|
+
}
|
|
94
|
+
const name = `${nodeName(entity, node)}Page`;
|
|
95
|
+
if (reserve(name, node))
|
|
96
|
+
schemas[name] = { type: 'object', required: ['data', 'total'], properties: { data: { type: 'array', items: output(entity, node) }, total: { type: 'integer', minimum: 0 } } };
|
|
97
|
+
return ref(name);
|
|
42
98
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
function where(entity, node) {
|
|
100
|
+
if (node.relation) {
|
|
101
|
+
entity = node.relation;
|
|
102
|
+
node = entity.root;
|
|
47
103
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (targetSchema.type === 'object' && isObject(targetSchema.properties) && !Object.hasOwn(targetSchema.properties, reverseRelationName)) {
|
|
58
|
-
schemas[targetComponentName] = {
|
|
59
|
-
...targetSchema,
|
|
60
|
-
properties: {
|
|
61
|
-
...targetSchema.properties,
|
|
62
|
-
[reverseRelationName]: { items: createSchemaReference(componentNames[sourceResource]), type: 'array' },
|
|
63
|
-
},
|
|
64
|
-
};
|
|
104
|
+
const name = `${nodeName(entity, node)}Where`;
|
|
105
|
+
if (!reserve(name, node))
|
|
106
|
+
return ref(name);
|
|
107
|
+
const properties = { and: { type: 'array', items: ref(name) }, or: { type: 'array', minItems: 1, items: ref(name) }, not: ref(name) };
|
|
108
|
+
for (const [key, child] of Object.entries(node.children))
|
|
109
|
+
if (!child.writeOnly) {
|
|
110
|
+
if (Object.hasOwn(properties, key))
|
|
111
|
+
throw new Error(`Reserved filter field: ${entity.name}.${child.path}`);
|
|
112
|
+
properties[key] = !child.many && (child.relation || child.base === 'object') ? where(entity, child) : filter(entity, child);
|
|
65
113
|
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const createParameterReference = (name) => ({ $ref: `#/components/parameters/${name}` });
|
|
90
|
-
const createRequestBody = (name) => ({ required: true, ...createJsonContent(createSchemaReference(name)) });
|
|
91
|
-
const createFilePaths = () => ({
|
|
92
|
-
[`${FILE_ROUTES.download}/{path}`]: {
|
|
93
|
-
get: {
|
|
94
|
-
operationId: 'downloadFile',
|
|
95
|
-
parameters: [createParameterReference('FilePath')],
|
|
96
|
-
responses: {
|
|
97
|
-
200: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, description: 'File download' },
|
|
98
|
-
400: createErrorResponse('Invalid path'),
|
|
99
|
-
404: createErrorResponse('Not found'),
|
|
100
|
-
},
|
|
101
|
-
tags: ['files'],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
[`${FILE_ROUTES.metadata}/{path}`]: {
|
|
105
|
-
get: {
|
|
106
|
-
operationId: 'getFileMetadata',
|
|
107
|
-
parameters: [createParameterReference('FilePath')],
|
|
108
|
-
responses: {
|
|
109
|
-
200: createResponse('File metadata', createSchemaReference('FileMetadata')),
|
|
110
|
-
400: createErrorResponse('Invalid path'),
|
|
111
|
-
404: createErrorResponse('Not found'),
|
|
112
|
-
},
|
|
113
|
-
tags: ['files'],
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
[FILE_ROUTES.storage]: {
|
|
117
|
-
post: {
|
|
118
|
-
operationId: 'uploadFile',
|
|
119
|
-
parameters: ['ContentName', 'ContentDirectory', 'ContentOverride'].map(createParameterReference),
|
|
120
|
-
requestBody: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, required: true },
|
|
121
|
-
responses: {
|
|
122
|
-
200: createResponse('Overwritten', createSchemaReference('FileMetadata')),
|
|
123
|
-
201: createResponse('Created', createSchemaReference('FileMetadata')),
|
|
124
|
-
400: createErrorResponse('Invalid request'),
|
|
125
|
-
409: createErrorResponse('Already exists'),
|
|
126
|
-
413: createErrorResponse('File is too large'),
|
|
127
|
-
415: createErrorResponse('Unsupported media type'),
|
|
128
|
-
},
|
|
129
|
-
tags: ['files'],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
[`${FILE_ROUTES.storage}/{path}`]: {
|
|
133
|
-
delete: {
|
|
134
|
-
operationId: 'deleteFile',
|
|
135
|
-
parameters: [createParameterReference('FilePath')],
|
|
136
|
-
responses: {
|
|
137
|
-
204: { description: 'Deleted' },
|
|
138
|
-
400: createErrorResponse('Invalid path'),
|
|
139
|
-
404: createErrorResponse('Not found'),
|
|
140
|
-
},
|
|
141
|
-
tags: ['files'],
|
|
142
|
-
},
|
|
143
|
-
get: {
|
|
144
|
-
operationId: 'getFileContent',
|
|
145
|
-
parameters: [createParameterReference('FilePath')],
|
|
146
|
-
responses: {
|
|
147
|
-
200: { content: { '*/*': { schema: { format: 'binary', type: 'string' } } }, description: 'File contents' },
|
|
148
|
-
400: createErrorResponse('Invalid path'),
|
|
149
|
-
404: createErrorResponse('Not found'),
|
|
150
|
-
},
|
|
151
|
-
tags: ['files'],
|
|
152
|
-
},
|
|
153
|
-
patch: {
|
|
154
|
-
operationId: 'updateFile',
|
|
155
|
-
parameters: [createParameterReference('FilePath')],
|
|
156
|
-
requestBody: createRequestBody('FileUpdate'),
|
|
157
|
-
responses: {
|
|
158
|
-
200: createResponse('Updated', createSchemaReference('FileMetadata')),
|
|
159
|
-
400: createErrorResponse('Invalid request'),
|
|
160
|
-
404: createErrorResponse('Not found'),
|
|
161
|
-
409: createErrorResponse('Already exists'),
|
|
162
|
-
413: createErrorResponse('Request is too large'),
|
|
163
|
-
415: createErrorResponse('Unsupported media type'),
|
|
164
|
-
},
|
|
165
|
-
tags: ['files'],
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
const createResourceOperationIds = (resource) => {
|
|
170
|
-
const resourceName = toPascalCase(resource);
|
|
171
|
-
return {
|
|
172
|
-
create: `post${resourceName}`,
|
|
173
|
-
get: `get${resourceName}ById`,
|
|
174
|
-
list: `get${resourceName}`,
|
|
175
|
-
remove: `delete${resourceName}ById`,
|
|
176
|
-
replace: `put${resourceName}ById`,
|
|
177
|
-
update: `patch${resourceName}ById`,
|
|
178
|
-
};
|
|
179
|
-
};
|
|
180
|
-
const createResourcePaths = (resource, componentName) => {
|
|
181
|
-
const operationIds = createResourceOperationIds(resource);
|
|
182
|
-
return {
|
|
183
|
-
[`/${resource}`]: {
|
|
184
|
-
get: {
|
|
185
|
-
operationId: operationIds.list,
|
|
186
|
-
parameters: ['Page', 'PerPage', 'Sort', 'Where', 'Embed'].map(createParameterReference),
|
|
187
|
-
responses: { 200: createResponse('Successful response', createSchemaReference(`${componentName}Page`)), 400: createErrorResponse('Invalid query') },
|
|
188
|
-
tags: [resource],
|
|
189
|
-
},
|
|
190
|
-
post: {
|
|
191
|
-
operationId: operationIds.create,
|
|
192
|
-
requestBody: createRequestBody(`${componentName}Create`),
|
|
193
|
-
responses: { 201: createResponse('Created', createSchemaReference(componentName)), 400: createErrorResponse('Invalid request') },
|
|
194
|
-
tags: [resource],
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
[`/${resource}/{id}`]: {
|
|
198
|
-
delete: {
|
|
199
|
-
operationId: operationIds.remove,
|
|
200
|
-
parameters: [createParameterReference('Id')],
|
|
201
|
-
responses: { 200: createResponse('Deleted', createSchemaReference(componentName)), 404: createErrorResponse('Not found') },
|
|
202
|
-
tags: [resource],
|
|
203
|
-
},
|
|
204
|
-
get: {
|
|
205
|
-
operationId: operationIds.get,
|
|
206
|
-
parameters: [createParameterReference('Id'), createParameterReference('Embed')],
|
|
207
|
-
responses: {
|
|
208
|
-
200: createResponse('Successful response', createSchemaReference(componentName)),
|
|
209
|
-
400: createErrorResponse('Invalid query'),
|
|
210
|
-
404: createErrorResponse('Not found'),
|
|
211
|
-
},
|
|
212
|
-
tags: [resource],
|
|
213
|
-
},
|
|
214
|
-
patch: {
|
|
215
|
-
operationId: operationIds.update,
|
|
216
|
-
parameters: [createParameterReference('Id')],
|
|
217
|
-
requestBody: createRequestBody(`${componentName}Update`),
|
|
218
|
-
responses: {
|
|
219
|
-
200: createResponse('Updated', createSchemaReference(componentName)),
|
|
220
|
-
400: createErrorResponse('Invalid request'),
|
|
221
|
-
404: createErrorResponse('Not found'),
|
|
222
|
-
},
|
|
223
|
-
tags: [resource],
|
|
224
|
-
},
|
|
225
|
-
put: {
|
|
226
|
-
operationId: operationIds.replace,
|
|
227
|
-
parameters: [createParameterReference('Id')],
|
|
228
|
-
requestBody: createRequestBody(`${componentName}Create`),
|
|
229
|
-
responses: {
|
|
230
|
-
200: createResponse('Replaced', createSchemaReference(componentName)),
|
|
231
|
-
400: createErrorResponse('Invalid request'),
|
|
232
|
-
404: createErrorResponse('Not found'),
|
|
233
|
-
},
|
|
234
|
-
tags: [resource],
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
};
|
|
239
|
-
const validateGeneratedNames = (resources, componentNames, files) => {
|
|
240
|
-
const schemaOwners = new Map([
|
|
241
|
-
['Error', 'встроенная схема ошибки'],
|
|
242
|
-
...(files
|
|
243
|
-
? [
|
|
244
|
-
['FileMetadata', 'встроенная схема метаданных файла'],
|
|
245
|
-
['FileUpdate', 'встроенная схема изменения файла'],
|
|
246
|
-
]
|
|
247
|
-
: []),
|
|
248
|
-
]);
|
|
249
|
-
resources.forEach((resource) => {
|
|
250
|
-
const componentName = componentNames[resource];
|
|
251
|
-
if (componentName === '') {
|
|
252
|
-
throw new Error(`Не удалось сформировать имя OpenAPI-схемы для ресурса «${resource}». Укажите $schema.${resource}.name`);
|
|
114
|
+
schemas[name] = { type: 'object', additionalProperties: false, properties };
|
|
115
|
+
return ref(name);
|
|
116
|
+
}
|
|
117
|
+
function filter(entity, node) {
|
|
118
|
+
const name = `${nodeName(entity, node)}Filter`;
|
|
119
|
+
if (!reserve(name, node))
|
|
120
|
+
return ref(name);
|
|
121
|
+
const properties = {};
|
|
122
|
+
const element = node.many ? (node.relation || node.base === 'object' ? where(entity, node) : filter(entity, { ...node, many: false, path: `${node.path}_element` })) : undefined;
|
|
123
|
+
for (const [key, operand] of Object.entries(operatorsFor(node))) {
|
|
124
|
+
const nullable = ['eq', 'ne', 'in'].includes(key);
|
|
125
|
+
const scalar = {
|
|
126
|
+
type: node.base,
|
|
127
|
+
...(nullable ? { nullable: true } : {}),
|
|
128
|
+
...(node.enum ? { enum: [...node.enum, ...(nullable && !node.enum.includes(null) ? [null] : [])] } : {}),
|
|
129
|
+
};
|
|
130
|
+
if (operand === 'condition')
|
|
131
|
+
properties[key] = ref(name);
|
|
132
|
+
else if (operand === 'element')
|
|
133
|
+
properties[key] = element;
|
|
134
|
+
else
|
|
135
|
+
properties[key] =
|
|
136
|
+
operand === 'values' ? { type: 'array', items: scalar } : operand === 'text' ? { type: 'string' } : operand === 'comparison' ? { type: node.base } : scalar;
|
|
253
137
|
}
|
|
254
|
-
[
|
|
255
|
-
|
|
256
|
-
if (owner != null) {
|
|
257
|
-
throw new Error(`Имя OpenAPI-схемы «${schemaName}» используется ресурсами «${owner}» и «${resource}». Укажите уникальный $schema.${resource}.name`);
|
|
258
|
-
}
|
|
259
|
-
schemaOwners.set(schemaName, resource);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
};
|
|
263
|
-
const validateOperationIds = (paths) => {
|
|
264
|
-
const operationOwners = new Map();
|
|
265
|
-
Object.entries(paths).forEach(([path, pathItem]) => {
|
|
266
|
-
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
267
|
-
if (!isObject(operation) || typeof operation.operationId !== 'string') {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const owner = operationOwners.get(operation.operationId);
|
|
271
|
-
if (owner != null) {
|
|
272
|
-
throw new Error(`operationId «${operation.operationId}» используется операциями «${owner}» и «${method.toUpperCase()} ${path}»`);
|
|
273
|
-
}
|
|
274
|
-
operationOwners.set(operation.operationId, `${method.toUpperCase()} ${path}`);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
};
|
|
278
|
-
/** Builds an OpenAPI document without runtime server addresses. */
|
|
279
|
-
export function buildOpenapiDocument(options) {
|
|
280
|
-
const { database, files = false, maxPageSize = DEFAULT_MAX_PAGE_SIZE, schema: schemaConfig = {} } = options ?? {};
|
|
281
|
-
if (typeof files !== 'boolean') {
|
|
282
|
-
throw new Error('Ключ files должен содержать boolean');
|
|
138
|
+
schemas[name] = { type: 'object', additionalProperties: false, properties };
|
|
139
|
+
return ref(name);
|
|
283
140
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
141
|
+
function order(entity, node) {
|
|
142
|
+
if (node.relation) {
|
|
143
|
+
entity = node.relation;
|
|
144
|
+
node = entity.root;
|
|
145
|
+
}
|
|
146
|
+
const name = `${nodeName(entity, node)}Order`;
|
|
147
|
+
if (reserve(name, node)) {
|
|
148
|
+
const paths = sortableFields(node);
|
|
149
|
+
schemas[name] = {
|
|
150
|
+
type: 'array',
|
|
151
|
+
...(paths.length
|
|
152
|
+
? {
|
|
153
|
+
items: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
required: ['field', 'direction'],
|
|
157
|
+
properties: { field: { type: 'string', enum: paths }, direction: { type: 'string', enum: ['ASC', 'DESC'] } },
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
: { maxItems: 0, items: { type: 'object' } }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return ref(name);
|
|
287
164
|
}
|
|
288
|
-
|
|
289
|
-
|
|
165
|
+
function options(entity, node) {
|
|
166
|
+
return { type: 'object', additionalProperties: false, properties: { where: where(entity, node), order: order(entity, node), pager: ref('Pager') } };
|
|
290
167
|
}
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
schemas[
|
|
318
|
-
schemas[`${componentName}Page`] = {
|
|
319
|
-
properties: {
|
|
320
|
-
data: { items: createSchemaReference(componentName), type: 'array' },
|
|
321
|
-
total: { minimum: 0, type: 'integer' },
|
|
322
|
-
},
|
|
323
|
-
required: ['data', 'total'],
|
|
168
|
+
const paths = {};
|
|
169
|
+
const operations = new Set();
|
|
170
|
+
for (const entity of model.entities.filter((e) => e.api.includes('openapi'))) {
|
|
171
|
+
const name = entity.name;
|
|
172
|
+
const op = operationName(entity);
|
|
173
|
+
for (const mode of ['create', 'replace', 'update']) {
|
|
174
|
+
const key = `${name}${mode[0].toUpperCase() + mode.slice(1)}`;
|
|
175
|
+
reserve(key, entity.root);
|
|
176
|
+
schemas[key] = annotateInput(toOpenapi(objectSchema(entity.root, mode, true)), entity.root, mode !== 'update');
|
|
177
|
+
}
|
|
178
|
+
const nestedName = `${name}Nested`;
|
|
179
|
+
reserve(nestedName, entity.root);
|
|
180
|
+
const nestedProperties = {};
|
|
181
|
+
const collect = (owner, node, prefix, seen) => {
|
|
182
|
+
for (const [key, child] of Object.entries(childrenOf(node))) {
|
|
183
|
+
if (child.writeOnly || (!child.relation && child.base !== 'object'))
|
|
184
|
+
continue;
|
|
185
|
+
const path = prefix + key;
|
|
186
|
+
if (child.many)
|
|
187
|
+
nestedProperties[path] = options(owner, child);
|
|
188
|
+
const target = child.relation?.root ?? child;
|
|
189
|
+
if (!seen.has(target))
|
|
190
|
+
collect(child.relation ?? owner, target, `${path}.`, new Set([...seen, target]));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
collect(entity, entity.root, '', new Set([entity.root]));
|
|
194
|
+
schemas[nestedName] = {
|
|
324
195
|
type: 'object',
|
|
196
|
+
properties: nestedProperties,
|
|
197
|
+
additionalProperties: { type: 'object', additionalProperties: false, properties: { where: { type: 'object' }, order: { type: 'array', items: { type: 'object' } }, pager: ref('Pager') } },
|
|
198
|
+
description: 'Path -> list options. Recursive paths are validated against the model at runtime.',
|
|
325
199
|
};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
200
|
+
const shape = [
|
|
201
|
+
{ in: 'query', name: 'scope', schema: { type: 'string' }, description: 'Own fields and explicit relations, e.g. *,movies(id,title). * excludes relations and writeOnly fields.' },
|
|
202
|
+
{ in: 'query', name: 'nested', ...json(ref(nestedName)) },
|
|
203
|
+
];
|
|
204
|
+
const list = [...shape, ...Object.entries(options(entity, entity.root).properties ?? {}).map(([key, schema]) => ({ in: 'query', name: key, ...json(schema) }))];
|
|
205
|
+
const key = { in: 'path', name: entity.primary, required: true, schema: baseField({ ...entity.fields[entity.primary], generated: undefined }) };
|
|
206
|
+
const errors = { 400: response('Invalid request', ref('Error')), 404: response('Not found', ref('Error')), 409: response('Conflict', ref('Error')) };
|
|
207
|
+
const make = (operationId, parameters, schema, mode) => {
|
|
208
|
+
if (operations.has(operationId))
|
|
209
|
+
throw new Error(`OpenAPI operation collision: ${operationId}`);
|
|
210
|
+
operations.add(operationId);
|
|
211
|
+
return {
|
|
212
|
+
operationId,
|
|
213
|
+
tags: [entity.collection],
|
|
214
|
+
parameters,
|
|
215
|
+
...(mode ? { requestBody: { required: true, ...json(ref(`${name}${mode[0].toUpperCase() + mode.slice(1)}`)) } } : {}),
|
|
216
|
+
responses: { [mode === 'create' ? 201 : 200]: response('Success', schema), ...errors },
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
paths[`/${entity.collection}`] = { get: make(`${op}List`, list, page(entity, entity.root)), post: make(`${op}Create`, shape, output(entity, entity.root), 'create') };
|
|
220
|
+
paths[`/${entity.collection}/{${entity.primary}}`] = {
|
|
221
|
+
get: make(op, [key, ...shape], output(entity, entity.root)),
|
|
222
|
+
put: make(`${op}Replace`, [key, ...shape], output(entity, entity.root), 'replace'),
|
|
223
|
+
patch: make(`${op}Update`, [key, ...shape], output(entity, entity.root), 'update'),
|
|
224
|
+
delete: make(`${op}Delete`, [key, ...shape], output(entity, entity.root)),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const parameters = {};
|
|
228
|
+
if (files) {
|
|
229
|
+
for (const [name, schema] of Object.entries({ FileMetadata: FILE_METADATA_SCHEMA, FileUpdate: FILE_UPDATE_SCHEMA })) {
|
|
230
|
+
if (schemas[name])
|
|
231
|
+
throw new Error(`OpenAPI schema collision: ${name}`);
|
|
232
|
+
schemas[name] = schema;
|
|
233
|
+
}
|
|
234
|
+
Object.assign(parameters, {
|
|
235
|
+
FilePath: { in: 'path', name: 'path', required: true, schema: { type: 'string' } },
|
|
236
|
+
ContentDirectory: { in: 'header', name: FILE_HEADERS.directory.name, schema: { type: 'string' } },
|
|
237
|
+
ContentName: { in: 'header', name: FILE_HEADERS.name.name, required: true, schema: { type: 'string' } },
|
|
238
|
+
ContentOverride: { in: 'header', name: FILE_HEADERS.override.name, schema: { type: 'string', enum: ['false', 'true'], default: 'false' } },
|
|
239
|
+
});
|
|
240
|
+
for (const [path, item] of Object.entries(createFilePaths())) {
|
|
241
|
+
if (paths[path])
|
|
242
|
+
throw new Error(`File path collision: ${path}`);
|
|
243
|
+
for (const operation of Object.values(item)) {
|
|
244
|
+
if (isObject(operation) && typeof operation.operationId === 'string') {
|
|
245
|
+
if (operations.has(operation.operationId))
|
|
246
|
+
throw new Error(`OpenAPI operation collision: ${operation.operationId}`);
|
|
247
|
+
operations.add(operation.operationId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
paths[path] = item;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { openapi: '3.0.3', info, components: { schemas, parameters }, paths };
|
|
337
254
|
}
|
|
338
255
|
//# sourceMappingURL=document.js.map
|