@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/relations.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { getRelationKeys, resolveRelationResource } from './relation-metadata.js';
|
|
2
|
-
import { createHttpError, getResourceNames, isIdEqual, isObject, isSafeKey, singularize, toArray } from './utils.js';
|
|
3
|
-
|
|
4
|
-
const findLocalRelation = (item, relation, targetResource) => {
|
|
5
|
-
const relationKey = getRelationKeys(relation, singularize(relation), targetResource, singularize(targetResource)).find((key) => Object.hasOwn(item, key));
|
|
6
|
-
|
|
7
|
-
if (relationKey == null) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return { ids: toArray(item[relationKey]).filter((id) => id != null), isMany: relationKey.endsWith('Ids') || Array.isArray(item[relationKey]) };
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const hasReference = (value, relationKeys, id) => {
|
|
15
|
-
if (Array.isArray(value)) {
|
|
16
|
-
return value.some((item) => hasReference(item, relationKeys, id));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (!isObject(value)) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return Object.entries(value).some(([key, nestedValue]) => {
|
|
24
|
-
if (!isSafeKey(key)) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (relationKeys.includes(key)) {
|
|
29
|
-
return toArray(nestedValue).some((nestedId) => isIdEqual(nestedId, id));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return hasReference(nestedValue, relationKeys, id);
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const getResourceIndex = (database, resource, indexes) => {
|
|
37
|
-
if (!indexes.has(resource)) {
|
|
38
|
-
const index = new Map();
|
|
39
|
-
|
|
40
|
-
database.data[resource].forEach((item) => {
|
|
41
|
-
if (isObject(item) && item.id != null && !index.has(item.id)) {
|
|
42
|
-
index.set(String(item.id), item);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
indexes.set(resource, index);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return indexes.get(resource);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const findRelatedValue = (database, item, sourceResource, relation, targetResource, indexes) => {
|
|
53
|
-
const targetItems = database.data[targetResource];
|
|
54
|
-
const localRelation = findLocalRelation(item, relation, targetResource);
|
|
55
|
-
|
|
56
|
-
if (!Array.isArray(targetItems)) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (localRelation != null) {
|
|
61
|
-
const targetIndex = getResourceIndex(database, targetResource, indexes);
|
|
62
|
-
const relatedItems = localRelation.ids.map((id) => targetIndex.get(String(id))).filter((targetItem) => targetItem != null);
|
|
63
|
-
|
|
64
|
-
return localRelation.isMany ? relatedItems : (relatedItems[0] ?? null);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (item.id == null) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const reverseRelationKeys = getRelationKeys(singularize(sourceResource));
|
|
72
|
-
|
|
73
|
-
if (relation === 'child' || relation === 'children') {
|
|
74
|
-
reverseRelationKeys.push('parentId', 'parentIds');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return targetItems.filter((targetItem) => hasReference(targetItem, reverseRelationKeys, item.id));
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const embedPath = (database, item, sourceResource, [relation, ...nestedRelations], indexes) => {
|
|
81
|
-
if (relation == null || !isSafeKey(relation)) {
|
|
82
|
-
return item;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const currentValue = item[relation];
|
|
86
|
-
const targetResource = resolveRelationResource(getResourceNames(database.data), relation, sourceResource);
|
|
87
|
-
const nestedSourceResource = targetResource ?? relation;
|
|
88
|
-
const localRelation = targetResource == null ? undefined : findLocalRelation(item, relation, targetResource);
|
|
89
|
-
|
|
90
|
-
if (localRelation == null) {
|
|
91
|
-
if (Array.isArray(currentValue)) {
|
|
92
|
-
return nestedRelations.length === 0
|
|
93
|
-
? item
|
|
94
|
-
: { ...item, [relation]: currentValue.map((value) => (isObject(value) ? embedPath(database, value, nestedSourceResource, nestedRelations, indexes) : value)) };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (isObject(currentValue)) {
|
|
98
|
-
return nestedRelations.length === 0 ? item : { ...item, [relation]: embedPath(database, currentValue, nestedSourceResource, nestedRelations, indexes) };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (targetResource == null) {
|
|
103
|
-
return item;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const relatedValue = findRelatedValue(database, item, sourceResource, relation, targetResource, indexes);
|
|
107
|
-
|
|
108
|
-
if (relatedValue == null || nestedRelations.length === 0) {
|
|
109
|
-
return relatedValue === undefined ? item : { ...item, [relation]: relatedValue };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
...item,
|
|
114
|
-
[relation]: Array.isArray(relatedValue)
|
|
115
|
-
? relatedValue.map((value) => embedPath(database, value, targetResource, nestedRelations, indexes))
|
|
116
|
-
: embedPath(database, relatedValue, targetResource, nestedRelations, indexes),
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export const parseEmbedPaths = (embed) => {
|
|
121
|
-
if (embed == null) {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const values = toArray(embed);
|
|
126
|
-
|
|
127
|
-
if (values.some((value) => typeof value !== 'string')) {
|
|
128
|
-
throw createHttpError(400, 'Параметр _embed должен содержать строковые пути связей');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return values
|
|
132
|
-
.flatMap((value) => value.split(','))
|
|
133
|
-
.map((path) => {
|
|
134
|
-
const keys = path.split('.');
|
|
135
|
-
|
|
136
|
-
if (keys.some((key) => key === '' || !isSafeKey(key))) {
|
|
137
|
-
throw createHttpError(400, `Недопустимый путь связи «${path}»`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return keys;
|
|
141
|
-
});
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const getNestedSamples = (samples, relation) =>
|
|
145
|
-
samples.flatMap((sample) => {
|
|
146
|
-
if (!isObject(sample) || !Object.hasOwn(sample, relation)) {
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return toArray(sample[relation]).filter(isObject);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const validateEmbedPath = (database, sourceResource, samples, [relation, ...nestedRelations], path = '') => {
|
|
154
|
-
const relationPath = path === '' ? relation : `${path}.${relation}`;
|
|
155
|
-
const targetResource = resolveRelationResource(getResourceNames(database.data), relation, sourceResource);
|
|
156
|
-
const nestedSamples = targetResource == null ? getNestedSamples(samples, relation) : database.data[targetResource];
|
|
157
|
-
|
|
158
|
-
if (targetResource == null && nestedSamples.length === 0) {
|
|
159
|
-
throw createHttpError(400, `Неизвестный путь связи «${relationPath}»`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (nestedRelations.length > 0) {
|
|
163
|
-
validateEmbedPath(database, targetResource ?? relation, nestedSamples, nestedRelations, relationPath);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
export const validateEmbedPaths = (database, resource, items, embedPaths) => {
|
|
168
|
-
embedPaths.forEach((path) => {
|
|
169
|
-
validateEmbedPath(database, resource, items, path);
|
|
170
|
-
});
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
export const embedItem = (database, item, resource, embedPaths, indexes = new Map()) => embedPaths.reduce((embeddedItem, path) => embedPath(database, embeddedItem, resource, path, indexes), item);
|
package/src/server.js
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import Fastify from 'fastify';
|
|
2
|
-
import { normalizeServerConfig } from './config.js';
|
|
3
|
-
import { DEFAULT_HOST, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_PAGE_SIZE, DEFAULT_PORT } from './constants.js';
|
|
4
|
-
import { createDatabaseStore, createId, findItem, getCollection } from './database.js';
|
|
5
|
-
import { createFileStore, registerFileRoutes } from './files.js';
|
|
6
|
-
import { resolveSchemaConfig } from './openapi/config.js';
|
|
7
|
-
import { buildOpenapiDocument } from './openapi/document.js';
|
|
8
|
-
import { createOpenapi, writeOpenapi } from './openapi/index.js';
|
|
9
|
-
import { matchesWhere, paginateItems, parsePagination, parseWhere, sortItems, validateWhere } from './query/index.js';
|
|
10
|
-
import { embedItem, parseEmbedPaths, validateEmbedPaths } from './relations.js';
|
|
11
|
-
import { createHttpError, getResourceNames, isObject } from './utils.js';
|
|
12
|
-
|
|
13
|
-
/** @typedef {Record<string, unknown>} OpenapiDocument */
|
|
14
|
-
/** @typedef {{ fastify: () => import('fastify').FastifyInstance, openapi: () => Promise<OpenapiDocument> }} ServerFacade */
|
|
15
|
-
|
|
16
|
-
const CORS_HEADERS = {
|
|
17
|
-
'Access-Control-Allow-Headers': 'Content-Directory, Content-Name, Content-Override, Content-Type',
|
|
18
|
-
'Access-Control-Allow-Methods': 'DELETE, GET, OPTIONS, PATCH, POST, PUT',
|
|
19
|
-
'Access-Control-Allow-Origin': '*',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const getJsonObjectBody = (body) => {
|
|
23
|
-
if (!isObject(body)) {
|
|
24
|
-
throw createHttpError(400, 'Тело запроса должно быть JSON-объектом');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return body;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const getSchemaName = (reference) => reference.split('/').at(-1);
|
|
31
|
-
|
|
32
|
-
const addRequestSchemas = (fastify, document, resources) => {
|
|
33
|
-
const requestSchemaNames = new Set(
|
|
34
|
-
resources.flatMap((resource) => {
|
|
35
|
-
const resourcePath = document.paths[`/${resource}`];
|
|
36
|
-
const itemPath = document.paths[`/${resource}/{id}`];
|
|
37
|
-
|
|
38
|
-
return [getSchemaName(resourcePath.post.requestBody.content['application/json'].schema.$ref), getSchemaName(itemPath.patch.requestBody.content['application/json'].schema.$ref)];
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
requestSchemaNames.forEach((schemaName) => {
|
|
43
|
-
fastify.addSchema({ $id: schemaName, ...document.components.schemas[schemaName] });
|
|
44
|
-
});
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const registerResourceRoutes = (fastify, store, resource, document, maxPageSize) => {
|
|
48
|
-
const resourcePath = `/${resource}`;
|
|
49
|
-
const itemPath = `/${resource}/:id`;
|
|
50
|
-
const createSchemaName = getSchemaName(document.paths[resourcePath].post.requestBody.content['application/json'].schema.$ref);
|
|
51
|
-
const updateSchemaName = getSchemaName(document.paths[`/${resource}/{id}`].patch.requestBody.content['application/json'].schema.$ref);
|
|
52
|
-
|
|
53
|
-
fastify.get(resourcePath, async (request) => {
|
|
54
|
-
const collection = getCollection(store.database, resource);
|
|
55
|
-
const where = parseWhere(request.query);
|
|
56
|
-
const embedPaths = parseEmbedPaths(request.query._embed);
|
|
57
|
-
const pagination = parsePagination(request.query, maxPageSize);
|
|
58
|
-
|
|
59
|
-
validateEmbedPaths(store.database, resource, collection, embedPaths);
|
|
60
|
-
|
|
61
|
-
const relationIndexes = new Map();
|
|
62
|
-
const embeddedItems = collection.map((item) => embedItem(store.database, item, resource, embedPaths, relationIndexes));
|
|
63
|
-
|
|
64
|
-
validateWhere(where, embeddedItems);
|
|
65
|
-
|
|
66
|
-
const filteredItems = embeddedItems.filter((item) => matchesWhere(item, where));
|
|
67
|
-
const sortedItems = sortItems(filteredItems, request.query._sort, embeddedItems);
|
|
68
|
-
|
|
69
|
-
return paginateItems(sortedItems, pagination.page, pagination.pageSize);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
fastify.get(itemPath, async (request) => {
|
|
73
|
-
const collection = getCollection(store.database, resource);
|
|
74
|
-
const item = findItem(collection, request.params.id);
|
|
75
|
-
const embedPaths = parseEmbedPaths(request.query._embed);
|
|
76
|
-
|
|
77
|
-
if (item == null) {
|
|
78
|
-
throw createHttpError(404, 'Запись не найдена');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
validateEmbedPaths(store.database, resource, collection, embedPaths);
|
|
82
|
-
|
|
83
|
-
return embedItem(store.database, item, resource, embedPaths);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
fastify.post(resourcePath, { schema: { body: { $ref: `${createSchemaName}#` } } }, async (request, reply) => {
|
|
87
|
-
const item = await store.update((database) => {
|
|
88
|
-
const collection = getCollection(database, resource);
|
|
89
|
-
const createdItem = { ...getJsonObjectBody(request.body), id: createId(collection) };
|
|
90
|
-
|
|
91
|
-
collection.push(createdItem);
|
|
92
|
-
|
|
93
|
-
return createdItem;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return reply.code(201).send(item);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
fastify.put(itemPath, { schema: { body: { $ref: `${createSchemaName}#` } } }, async (request) =>
|
|
100
|
-
store.update((database) => {
|
|
101
|
-
const collection = getCollection(database, resource);
|
|
102
|
-
const currentItem = findItem(collection, request.params.id);
|
|
103
|
-
|
|
104
|
-
if (currentItem == null) {
|
|
105
|
-
throw createHttpError(404, 'Запись не найдена');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const item = { ...getJsonObjectBody(request.body), id: currentItem.id };
|
|
109
|
-
|
|
110
|
-
collection.splice(collection.indexOf(currentItem), 1, item);
|
|
111
|
-
|
|
112
|
-
return item;
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
fastify.patch(itemPath, { schema: { body: { $ref: `${updateSchemaName}#` } } }, async (request) =>
|
|
117
|
-
store.update((database) => {
|
|
118
|
-
const collection = getCollection(database, resource);
|
|
119
|
-
const currentItem = findItem(collection, request.params.id);
|
|
120
|
-
|
|
121
|
-
if (currentItem == null) {
|
|
122
|
-
throw createHttpError(404, 'Запись не найдена');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const item = { ...currentItem, ...getJsonObjectBody(request.body), id: currentItem.id };
|
|
126
|
-
|
|
127
|
-
collection.splice(collection.indexOf(currentItem), 1, item);
|
|
128
|
-
|
|
129
|
-
return item;
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
fastify.delete(itemPath, async (request) =>
|
|
134
|
-
store.update((database) => {
|
|
135
|
-
const collection = getCollection(database, resource);
|
|
136
|
-
const currentItem = findItem(collection, request.params.id);
|
|
137
|
-
|
|
138
|
-
if (currentItem == null) {
|
|
139
|
-
throw createHttpError(404, 'Запись не найдена');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
collection.splice(collection.indexOf(currentItem), 1);
|
|
143
|
-
|
|
144
|
-
return currentItem;
|
|
145
|
-
}),
|
|
146
|
-
);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Creates lazy Fastify and OpenAPI accessors from one configuration.
|
|
151
|
-
* @param {import('./config.js').DeepJsonServerConfig} config Server configuration.
|
|
152
|
-
* @param {{ files?: boolean }} [features] Optional feature switches.
|
|
153
|
-
* @returns {Promise<ServerFacade>} Server facade.
|
|
154
|
-
*/
|
|
155
|
-
export async function createServer(config, features = {}) {
|
|
156
|
-
const normalizedConfig = normalizeServerConfig(config);
|
|
157
|
-
const filesEnabled = features.files ?? normalizedConfig.files != null;
|
|
158
|
-
const { logger = true, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxPageSize = DEFAULT_MAX_PAGE_SIZE } = normalizedConfig.server;
|
|
159
|
-
const store = await createDatabaseStore(normalizedConfig.database);
|
|
160
|
-
const schema = await resolveSchemaConfig(normalizedConfig.database.schema);
|
|
161
|
-
const fileStore = filesEnabled && normalizedConfig.files != null ? await createFileStore(normalizedConfig.files) : undefined;
|
|
162
|
-
let fastifyInstance;
|
|
163
|
-
|
|
164
|
-
if (filesEnabled && normalizedConfig.files == null) {
|
|
165
|
-
throw new Error('Для файловых маршрутов укажите секцию config.files');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const buildDocument = () =>
|
|
169
|
-
buildOpenapiDocument({
|
|
170
|
-
database: store.database.data,
|
|
171
|
-
files: filesEnabled,
|
|
172
|
-
maxPageSize,
|
|
173
|
-
schema,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const getFastify = () => {
|
|
177
|
-
if (fastifyInstance != null) {
|
|
178
|
-
return fastifyInstance;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const document = buildDocument();
|
|
182
|
-
const resources = getResourceNames(store.database.data);
|
|
183
|
-
const fastify = Fastify({ logger });
|
|
184
|
-
const originalListen = fastify.listen.bind(fastify);
|
|
185
|
-
|
|
186
|
-
// Calling fastify().listen() without arguments uses config defaults.
|
|
187
|
-
fastify.listen = (...args) => originalListen(...(args.length === 0 ? [{ host: normalizedConfig.server.host ?? DEFAULT_HOST, port: normalizedConfig.server.port ?? DEFAULT_PORT }] : args));
|
|
188
|
-
|
|
189
|
-
addRequestSchemas(fastify, document, resources);
|
|
190
|
-
|
|
191
|
-
fastify.addHook('onRequest', async (_request, reply) => {
|
|
192
|
-
Object.entries(CORS_HEADERS).forEach(([header, value]) => {
|
|
193
|
-
reply.header(header, value);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
fastify.addHook('preHandler', async (request) => {
|
|
198
|
-
if (request.method === 'GET') {
|
|
199
|
-
await store.read();
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
fastify.options('/', async (_request, reply) => reply.code(204).send());
|
|
204
|
-
fastify.options('/*', async (_request, reply) => reply.code(204).send());
|
|
205
|
-
fastify.get('/', async () => ({ resources }));
|
|
206
|
-
|
|
207
|
-
resources.forEach((resource) => {
|
|
208
|
-
registerResourceRoutes(fastify, store, resource, document, maxPageSize);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
if (fileStore != null) {
|
|
212
|
-
registerFileRoutes(fastify, { maxFileSize, store: fileStore });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fastify.setErrorHandler((error, request, reply) => {
|
|
216
|
-
const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
|
|
217
|
-
|
|
218
|
-
if (statusCode === 500) {
|
|
219
|
-
request.log.error(error);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return reply.code(statusCode).send({ error: error.message });
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
fastifyInstance = fastify;
|
|
226
|
-
|
|
227
|
-
return fastifyInstance;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const openapi = async () => {
|
|
231
|
-
await store.read();
|
|
232
|
-
|
|
233
|
-
const document = createOpenapi({
|
|
234
|
-
database: store.database.data,
|
|
235
|
-
files: filesEnabled,
|
|
236
|
-
host: normalizedConfig.server.host,
|
|
237
|
-
maxPageSize,
|
|
238
|
-
port: normalizedConfig.server.port,
|
|
239
|
-
schema,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
if (normalizedConfig.openapi.path != null) {
|
|
243
|
-
await writeOpenapi(document, normalizedConfig.openapi.path);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return document;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
return { fastify: getFastify, openapi };
|
|
250
|
-
}
|
package/src/utils.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import pluralize from 'pluralize';
|
|
4
|
-
|
|
5
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
|
-
|
|
7
|
-
export const createHttpError = (statusCode, message) => {
|
|
8
|
-
const error = new Error(message);
|
|
9
|
-
|
|
10
|
-
error.statusCode = statusCode;
|
|
11
|
-
|
|
12
|
-
return error;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** @returns {<T>(operation: () => T | Promise<T>) => Promise<T>} Serialized operation scheduler. */
|
|
16
|
-
export const createSerialQueue = () => {
|
|
17
|
-
let queue = Promise.resolve();
|
|
18
|
-
|
|
19
|
-
return (operation) => {
|
|
20
|
-
const pendingOperation = queue.then(operation);
|
|
21
|
-
|
|
22
|
-
// Keep the queue usable after a failed operation.
|
|
23
|
-
queue = pendingOperation.catch(() => undefined);
|
|
24
|
-
|
|
25
|
-
return pendingOperation;
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Creates a random ID that is not currently in use.
|
|
31
|
-
* @param {(id: string) => boolean} isUsed Checks whether an ID already exists.
|
|
32
|
-
* @returns {string} Unique ID.
|
|
33
|
-
*/
|
|
34
|
-
export const createUniqueId = (isUsed) => {
|
|
35
|
-
let id;
|
|
36
|
-
|
|
37
|
-
do {
|
|
38
|
-
id = randomBytes(8).toString('base64url');
|
|
39
|
-
} while (isUsed(id));
|
|
40
|
-
|
|
41
|
-
return id;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const getResourceNames = (data) => Object.keys(data);
|
|
45
|
-
export const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
-
export const isSafeKey = (key) => !UNSAFE_KEYS.has(key);
|
|
47
|
-
export const toArray = (value) => (Array.isArray(value) ? value : [value]);
|
|
48
|
-
|
|
49
|
-
export const isEqual = (left, right) => {
|
|
50
|
-
if (Object.is(left, right)) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
55
|
-
return left.length === right.length && left.every((value, index) => isEqual(value, right[index]));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (isObject(left) && isObject(right)) {
|
|
59
|
-
const leftKeys = Object.keys(left);
|
|
60
|
-
const rightKeys = Object.keys(right);
|
|
61
|
-
|
|
62
|
-
return leftKeys.length === rightKeys.length && leftKeys.every((key) => isSafeKey(key) && Object.hasOwn(right, key) && isEqual(left[key], right[key]));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return false;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const resolveDatabasePath = (databasePath) => {
|
|
69
|
-
if (typeof databasePath !== 'string' || databasePath === '') {
|
|
70
|
-
throw new Error('Укажите путь к JSON-базе данных');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return resolve(databasePath);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const isIdEqual = (left, right) => left != null && right != null && String(left) === String(right);
|
|
77
|
-
export const singularize = (value) => pluralize.singular(value);
|
|
78
|
-
|
|
79
|
-
export const toPascalCase = (value) =>
|
|
80
|
-
value
|
|
81
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
84
|
-
.join('');
|
package/types/index.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** @typedef {import('./src/config.js').DeepJsonServerConfig} DeepJsonServerConfig */
|
|
2
|
-
/** @typedef {import('./src/config.js').DatabaseConfig} DatabaseConfig */
|
|
3
|
-
/** @typedef {import('./src/config.js').FilesConfig} FilesConfig */
|
|
4
|
-
/** @typedef {import('./src/config.js').MemoryFile} MemoryFile */
|
|
5
|
-
/** @typedef {import('./src/config.js').OpenapiConfig} OpenapiConfig */
|
|
6
|
-
/** @typedef {import('./src/config.js').ServerConfig} ServerConfig */
|
|
7
|
-
/** @typedef {import('./src/files.js').FileMetadata} FileMetadata */
|
|
8
|
-
/** @typedef {import('./src/files.js').FileUpdate} FileUpdate */
|
|
9
|
-
/** @typedef {import('./src/server.js').OpenapiDocument} OpenapiDocument */
|
|
10
|
-
/** @typedef {import('./src/server.js').ServerFacade} ServerFacade */
|
|
11
|
-
export type DeepJsonServerConfig = import('./src/config.js').DeepJsonServerConfig;
|
|
12
|
-
export type DatabaseConfig = import('./src/config.js').DatabaseConfig;
|
|
13
|
-
export type FilesConfig = import('./src/config.js').FilesConfig;
|
|
14
|
-
export type MemoryFile = import('./src/config.js').MemoryFile;
|
|
15
|
-
export type OpenapiConfig = import('./src/config.js').OpenapiConfig;
|
|
16
|
-
export type ServerConfig = import('./src/config.js').ServerConfig;
|
|
17
|
-
export type FileMetadata = import('./src/files.js').FileMetadata;
|
|
18
|
-
export type FileUpdate = import('./src/files.js').FileUpdate;
|
|
19
|
-
export type OpenapiDocument = import('./src/server.js').OpenapiDocument;
|
|
20
|
-
export type ServerFacade = import('./src/server.js').ServerFacade;
|
|
21
|
-
export { createServer } from './src/server.js';
|
package/types/src/config.d.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
export type DatabaseData = Record<string, Array<Record<string, unknown>>>;
|
|
2
|
-
export type DatabaseSchema = Record<string, unknown>;
|
|
3
|
-
export type DatabaseConfig = {
|
|
4
|
-
data: DatabaseData;
|
|
5
|
-
path?: never;
|
|
6
|
-
schema?: DatabaseSchema | string;
|
|
7
|
-
} | {
|
|
8
|
-
data?: never;
|
|
9
|
-
path: string;
|
|
10
|
-
schema?: DatabaseSchema | string;
|
|
11
|
-
};
|
|
12
|
-
export type MemoryFile = {
|
|
13
|
-
content: Uint8Array;
|
|
14
|
-
directory?: string;
|
|
15
|
-
mimeType: string;
|
|
16
|
-
name: string;
|
|
17
|
-
};
|
|
18
|
-
export type FilesConfig = {
|
|
19
|
-
data: MemoryFile[];
|
|
20
|
-
directory?: never;
|
|
21
|
-
metadata?: never;
|
|
22
|
-
} | {
|
|
23
|
-
data?: never;
|
|
24
|
-
directory: string;
|
|
25
|
-
metadata: string;
|
|
26
|
-
};
|
|
27
|
-
export type OpenapiConfig = {
|
|
28
|
-
path?: string;
|
|
29
|
-
};
|
|
30
|
-
export type ServerConfig = {
|
|
31
|
-
host?: string;
|
|
32
|
-
logger?: boolean | Record<string, unknown>;
|
|
33
|
-
maxFileSize?: number;
|
|
34
|
-
maxPageSize?: number;
|
|
35
|
-
port?: number;
|
|
36
|
-
};
|
|
37
|
-
export type DeepJsonServerConfig = {
|
|
38
|
-
/**
|
|
39
|
-
* Database source and optional schema.
|
|
40
|
-
*/
|
|
41
|
-
database: DatabaseConfig;
|
|
42
|
-
/**
|
|
43
|
-
* Binary-file storage.
|
|
44
|
-
*/
|
|
45
|
-
files?: FilesConfig;
|
|
46
|
-
/**
|
|
47
|
-
* Generated OpenAPI file.
|
|
48
|
-
*/
|
|
49
|
-
openapi?: OpenapiConfig;
|
|
50
|
-
/**
|
|
51
|
-
* Runtime settings.
|
|
52
|
-
*/
|
|
53
|
-
server?: ServerConfig;
|
|
54
|
-
};
|
|
55
|
-
export type NormalizedServerConfig = {
|
|
56
|
-
database: DatabaseConfig;
|
|
57
|
-
files?: FilesConfig;
|
|
58
|
-
openapi: OpenapiConfig;
|
|
59
|
-
server: ServerConfig;
|
|
60
|
-
};
|
|
61
|
-
/**
|
|
62
|
-
* Validates configuration and resolves relative paths.
|
|
63
|
-
* @param {DeepJsonServerConfig} config Server configuration.
|
|
64
|
-
* @param {string} [directoryPath] Base directory for relative paths.
|
|
65
|
-
* @returns {NormalizedServerConfig} Normalized configuration.
|
|
66
|
-
*/
|
|
67
|
-
export declare const normalizeServerConfig: (config: DeepJsonServerConfig, directoryPath?: string) => NormalizedServerConfig;
|
|
68
|
-
/**
|
|
69
|
-
* Loads an ES module config and resolves paths from its directory.
|
|
70
|
-
* @param {string} configPath Configuration module path.
|
|
71
|
-
* @returns {Promise<NormalizedServerConfig>} Normalized configuration.
|
|
72
|
-
*/
|
|
73
|
-
export declare function readServerConfig(configPath: string): Promise<NormalizedServerConfig>;
|
package/types/src/database.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type DatabaseContainer = {
|
|
2
|
-
data: import('./config.js').DatabaseData;
|
|
3
|
-
};
|
|
4
|
-
export type DatabaseStore = {
|
|
5
|
-
/**
|
|
6
|
-
* Current database container.
|
|
7
|
-
*/
|
|
8
|
-
database: DatabaseContainer;
|
|
9
|
-
/**
|
|
10
|
-
* Resolved database file path.
|
|
11
|
-
*/
|
|
12
|
-
path?: string;
|
|
13
|
-
/**
|
|
14
|
-
* Returns current data and reloads disk-backed sources.
|
|
15
|
-
*/
|
|
16
|
-
read: () => Promise<import('./config.js').DatabaseData>;
|
|
17
|
-
/**
|
|
18
|
-
* Runs a serialized update.
|
|
19
|
-
*/
|
|
20
|
-
update: <T>(operation: (database: DatabaseContainer) => T) => Promise<T>;
|
|
21
|
-
};
|
|
22
|
-
export declare const validateDatabase: (data: any) => any;
|
|
23
|
-
export declare const readJsonObject: (path: any, label: any) => Promise<any>;
|
|
24
|
-
export declare const readDatabaseFile: (databasePath: any) => Promise<any>;
|
|
25
|
-
/**
|
|
26
|
-
* Creates a disk- or memory-backed database with serialized updates.
|
|
27
|
-
* @param {import('./config.js').DatabaseConfig} config Database source.
|
|
28
|
-
* @returns {Promise<DatabaseStore>} Database store.
|
|
29
|
-
*/
|
|
30
|
-
export declare const createDatabaseStore: (config: import('./config.js').DatabaseConfig) => Promise<DatabaseStore>;
|
|
31
|
-
export declare const getCollection: (database: any, resource: any) => any[];
|
|
32
|
-
export declare const findItem: (collection: any, id: any) => any;
|
|
33
|
-
export declare const createId: (collection: any) => string;
|