@kollors/deep-json-server 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +19 -16
  2. package/README.ru.md +20 -17
  3. package/dist/bin/deep-json-server.d.ts +2 -0
  4. package/dist/bin/deep-json-server.js +11 -0
  5. package/dist/bin/deep-json-server.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/src/cli.d.ts +4 -0
  10. package/dist/src/cli.js +80 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +54 -0
  13. package/dist/src/config.js +145 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/{src → dist/src}/constants.js +1 -0
  16. package/dist/src/constants.js.map +1 -0
  17. package/dist/src/database.d.ts +20 -0
  18. package/dist/src/database.js +136 -0
  19. package/dist/src/database.js.map +1 -0
  20. package/dist/src/files/contract.d.ts +67 -0
  21. package/dist/src/files/contract.js +106 -0
  22. package/dist/src/files/contract.js.map +1 -0
  23. package/dist/src/files/disk-store.d.ts +5 -0
  24. package/dist/src/files/disk-store.js +313 -0
  25. package/dist/src/files/disk-store.js.map +1 -0
  26. package/dist/src/files/index.d.ts +5 -0
  27. package/dist/src/files/index.js +6 -0
  28. package/dist/src/files/index.js.map +1 -0
  29. package/dist/src/files/memory-store.d.ts +3 -0
  30. package/dist/src/files/memory-store.js +80 -0
  31. package/dist/src/files/memory-store.js.map +1 -0
  32. package/dist/src/files/routes.d.ts +6 -0
  33. package/dist/src/files/routes.js +77 -0
  34. package/dist/src/files/routes.js.map +1 -0
  35. package/dist/src/openapi/config.d.ts +11 -0
  36. package/dist/src/openapi/config.js +204 -0
  37. package/dist/src/openapi/config.js.map +1 -0
  38. package/dist/src/openapi/document.d.ts +9 -0
  39. package/dist/src/openapi/document.js +338 -0
  40. package/dist/src/openapi/document.js.map +1 -0
  41. package/dist/src/openapi/index.d.ts +10 -0
  42. package/dist/src/openapi/index.js +27 -0
  43. package/dist/src/openapi/index.js.map +1 -0
  44. package/dist/src/openapi/inference.d.ts +14 -0
  45. package/dist/src/openapi/inference.js +145 -0
  46. package/dist/src/openapi/inference.js.map +1 -0
  47. package/dist/src/query/filter.d.ts +6 -0
  48. package/dist/src/query/filter.js +248 -0
  49. package/dist/src/query/filter.js.map +1 -0
  50. package/{src → dist/src}/query/index.js +1 -0
  51. package/dist/src/query/index.js.map +1 -0
  52. package/dist/src/query/pagination.d.ts +11 -0
  53. package/dist/src/query/pagination.js +28 -0
  54. package/dist/src/query/pagination.js.map +1 -0
  55. package/dist/src/query/sort.d.ts +1 -0
  56. package/dist/src/query/sort.js +54 -0
  57. package/dist/src/query/sort.js.map +1 -0
  58. package/dist/src/relation-metadata.d.ts +10 -0
  59. package/dist/src/relation-metadata.js +32 -0
  60. package/dist/src/relation-metadata.js.map +1 -0
  61. package/dist/src/relations.d.ts +12 -0
  62. package/dist/src/relations.js +155 -0
  63. package/dist/src/relations.js.map +1 -0
  64. package/dist/src/server.d.ts +13 -0
  65. package/dist/src/server.js +188 -0
  66. package/dist/src/server.js.map +1 -0
  67. package/dist/src/types.d.ts +52 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils.d.ts +20 -0
  71. package/dist/src/utils.js +63 -0
  72. package/dist/src/utils.js.map +1 -0
  73. package/package.json +17 -16
  74. package/bin/deep-json-server.js +0 -11
  75. package/index.js +0 -12
  76. package/src/cli.js +0 -91
  77. package/src/config.js +0 -203
  78. package/src/database.js +0 -148
  79. package/src/files.js +0 -633
  80. package/src/openapi/config.js +0 -124
  81. package/src/openapi/document.js +0 -394
  82. package/src/openapi/index.js +0 -36
  83. package/src/openapi/inference.js +0 -187
  84. package/src/query/filter.js +0 -283
  85. package/src/query/pagination.js +0 -44
  86. package/src/query/sort.js +0 -63
  87. package/src/relation-metadata.js +0 -41
  88. package/src/relations.js +0 -173
  89. package/src/server.js +0 -250
  90. package/src/utils.js +0 -84
  91. package/types/index.d.ts +0 -21
  92. package/types/src/config.d.ts +0 -73
  93. package/types/src/database.d.ts +0 -33
  94. package/types/src/files.d.ts +0 -64
  95. package/types/src/openapi/config.d.ts +0 -3
  96. package/types/src/openapi/document.d.ts +0 -11
  97. package/types/src/openapi/index.d.ts +0 -9
  98. package/types/src/openapi/inference.d.ts +0 -9
  99. package/types/src/query/filter.d.ts +0 -3
  100. package/types/src/query/pagination.d.ts +0 -8
  101. package/types/src/query/sort.d.ts +0 -1
  102. package/types/src/relation-metadata.d.ts +0 -9
  103. package/types/src/relations.d.ts +0 -3
  104. package/types/src/server.d.ts +0 -14
  105. package/types/src/utils.d.ts +0 -18
  106. /package/{types → dist}/src/constants.d.ts +0 -0
  107. /package/{types → dist}/src/query/index.d.ts +0 -0
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import process from 'node:process';
4
- import { runCli } from '../src/cli.js';
5
-
6
- try {
7
- await runCli();
8
- } catch (error) {
9
- process.stderr.write(`${error.message}\n`);
10
- process.exitCode = 1;
11
- }
package/index.js DELETED
@@ -1,12 +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
-
12
- export { createServer } from './src/server.js';
package/src/cli.js DELETED
@@ -1,91 +0,0 @@
1
- import process from 'node:process';
2
- import { readServerConfig } from './config.js';
3
- import { DEFAULT_HOST, DEFAULT_PORT } from './constants.js';
4
- import { createServer } from './server.js';
5
-
6
- const HELP_TEXT = `Deep JSON Server
7
-
8
- Использование:
9
- deep-json-server [--files] [--openapi | --openapi-only] <server.config.js>
10
-
11
- Параметры:
12
- --files Добавить файловые маршруты в сервер и OpenAPI
13
- --openapi Сгенерировать OpenAPI и запустить сервер
14
- --openapi-only Сгенерировать OpenAPI и завершить работу
15
- --help Показать справку`;
16
-
17
- const parseArguments = (args) => {
18
- const options = { files: false, openapiMode: 'none' };
19
- let configPath;
20
-
21
- args.forEach((argument) => {
22
- if (argument === '--files') {
23
- options.files = true;
24
- } else if (argument === '--openapi') {
25
- if (options.openapiMode !== 'none') {
26
- throw new Error('Параметры --openapi и --openapi-only нельзя использовать одновременно');
27
- }
28
-
29
- options.openapiMode = 'generate';
30
- } else if (argument === '--openapi-only') {
31
- if (options.openapiMode !== 'none') {
32
- throw new Error('Параметры --openapi и --openapi-only нельзя использовать одновременно');
33
- }
34
-
35
- options.openapiMode = 'only';
36
- } else if (argument.startsWith('-')) {
37
- throw new Error(`Неизвестный параметр: ${argument}`);
38
- } else if (configPath == null) {
39
- configPath = argument;
40
- } else {
41
- throw new Error('Можно указать только один файл конфигурации');
42
- }
43
- });
44
-
45
- if (configPath == null) {
46
- throw new Error('Укажите путь к файлу конфигурации');
47
- }
48
-
49
- return { configPath, ...options };
50
- };
51
-
52
- const validateModeConfig = (config, { files, openapiMode }) => {
53
- if (openapiMode !== 'none' && config.openapi.path == null) {
54
- throw new Error(`Для --openapi${openapiMode === 'only' ? '-only' : ''} укажите ключ config.openapi.path`);
55
- }
56
-
57
- if (files && config.files == null) {
58
- throw new Error('Для --files укажите секцию config.files');
59
- }
60
- };
61
-
62
- export async function runCli(args = process.argv.slice(2), services = { createServer }) {
63
- if (args.includes('--help')) {
64
- process.stdout.write(`${HELP_TEXT}\n`);
65
- return;
66
- }
67
-
68
- const { configPath, files, openapiMode } = parseArguments(args);
69
- const config = await readServerConfig(configPath);
70
- const host = config.server.host ?? process.env.HOST ?? DEFAULT_HOST;
71
- const port = config.server.port ?? Number(process.env.PORT ?? DEFAULT_PORT);
72
-
73
- validateModeConfig(config, { files, openapiMode });
74
-
75
- const runtimeConfig = { ...config, server: { ...config.server, host, port } };
76
- const serverFacade = await services.createServer(runtimeConfig, { files });
77
-
78
- if (openapiMode !== 'none') {
79
- await serverFacade.openapi();
80
- process.stdout.write(`OpenAPI-схема сохранена в ${config.openapi.path}\n`);
81
- }
82
-
83
- if (openapiMode === 'only') {
84
- return;
85
- }
86
-
87
- const fastify = serverFacade.fastify();
88
-
89
- await fastify.listen({ host, port });
90
- fastify.log.info({ database: 'path' in config.database ? config.database.path : 'memory' }, 'Deep JSON Server запущен');
91
- }
package/src/config.js DELETED
@@ -1,203 +0,0 @@
1
- import { dirname, resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { isObject } from './utils.js';
4
-
5
- const CONFIG_KEYS = new Set(['database', 'files', 'openapi', 'server']);
6
- const DATABASE_KEYS = new Set(['data', 'path', 'schema']);
7
- const FILES_KEYS = new Set(['data', 'directory', 'metadata']);
8
- const OPENAPI_KEYS = new Set(['path']);
9
- const SERVER_KEYS = new Set(['host', 'logger', 'maxFileSize', 'maxPageSize', 'port']);
10
- let configImportIndex = 0;
11
-
12
- /** @typedef {Record<string, Array<Record<string, unknown>>>} DatabaseData */
13
- /** @typedef {Record<string, unknown>} DatabaseSchema */
14
- /** @typedef {{ data: DatabaseData, path?: never, schema?: DatabaseSchema | string } | { data?: never, path: string, schema?: DatabaseSchema | string }} DatabaseConfig */
15
- /** @typedef {{ content: Uint8Array, directory?: string, mimeType: string, name: string }} MemoryFile */
16
- /** @typedef {{ data: MemoryFile[], directory?: never, metadata?: never } | { data?: never, directory: string, metadata: string }} FilesConfig */
17
- /** @typedef {{ path?: string }} OpenapiConfig */
18
- /** @typedef {{ host?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, port?: number }} ServerConfig */
19
- /**
20
- * @typedef {object} DeepJsonServerConfig
21
- * @property {DatabaseConfig} database Database source and optional schema.
22
- * @property {FilesConfig} [files] Binary-file storage.
23
- * @property {OpenapiConfig} [openapi] Generated OpenAPI file.
24
- * @property {ServerConfig} [server] Runtime settings.
25
- */
26
- /** @typedef {{ database: DatabaseConfig, files?: FilesConfig, openapi: OpenapiConfig, server: ServerConfig }} NormalizedServerConfig */
27
-
28
- const assertKnownKeys = (value, keys, path) => {
29
- const unknownKey = Object.keys(value).find((key) => !keys.has(key));
30
-
31
- if (unknownKey != null) {
32
- throw new Error(`Неизвестный ключ ${path}.${unknownKey}`);
33
- }
34
- };
35
-
36
- const getObject = (value, path, required = false) => {
37
- if (value == null && !required) {
38
- return undefined;
39
- }
40
-
41
- if (!isObject(value)) {
42
- throw new Error(`Ключ ${path} должен быть JSON-объектом`);
43
- }
44
-
45
- return value;
46
- };
47
-
48
- const getString = (value, path, required = false) => {
49
- if (value == null && !required) {
50
- return undefined;
51
- }
52
-
53
- if (typeof value !== 'string' || value.trim() === '') {
54
- throw new Error(`Ключ ${path} должен содержать непустую строку`);
55
- }
56
-
57
- return value;
58
- };
59
-
60
- const resolveConfigPath = (value, directoryPath) => (value == null ? undefined : resolve(directoryPath, value));
61
-
62
- const normalizeSchema = (schema, directoryPath) => {
63
- if (schema == null) {
64
- return undefined;
65
- }
66
-
67
- if (typeof schema === 'string') {
68
- return resolveConfigPath(getString(schema, 'config.database.schema', true), directoryPath);
69
- }
70
-
71
- return getObject(schema, 'config.database.schema', true);
72
- };
73
-
74
- const normalizeDatabase = (value, directoryPath) => {
75
- const database = getObject(value, 'config.database', true);
76
-
77
- assertKnownKeys(database, DATABASE_KEYS, 'config.database');
78
-
79
- const hasData = database.data != null;
80
- const hasPath = database.path != null;
81
-
82
- if (hasData === hasPath) {
83
- throw new Error('Укажите ровно один из ключей config.database.path и config.database.data');
84
- }
85
-
86
- const schema = normalizeSchema(database.schema, directoryPath);
87
-
88
- if (hasData) {
89
- return { data: getObject(database.data, 'config.database.data', true), schema };
90
- }
91
-
92
- return { path: resolveConfigPath(getString(database.path, 'config.database.path', true), directoryPath), schema };
93
- };
94
-
95
- const normalizeFiles = (value, directoryPath) => {
96
- const files = getObject(value, 'config.files');
97
-
98
- if (files == null) {
99
- return undefined;
100
- }
101
-
102
- assertKnownKeys(files, FILES_KEYS, 'config.files');
103
-
104
- const hasData = files.data != null;
105
- const hasDiskStorage = files.directory != null || files.metadata != null;
106
-
107
- if (hasData === hasDiskStorage) {
108
- throw new Error('Укажите либо config.files.data, либо пару config.files.directory и config.files.metadata');
109
- }
110
-
111
- if (hasData) {
112
- if (!Array.isArray(files.data)) {
113
- throw new Error('Ключ config.files.data должен содержать массив');
114
- }
115
-
116
- return { data: files.data };
117
- }
118
-
119
- return {
120
- directory: resolveConfigPath(getString(files.directory, 'config.files.directory', true), directoryPath),
121
- metadata: resolveConfigPath(getString(files.metadata, 'config.files.metadata', true), directoryPath),
122
- };
123
- };
124
-
125
- /**
126
- * Validates configuration and resolves relative paths.
127
- * @param {DeepJsonServerConfig} config Server configuration.
128
- * @param {string} [directoryPath] Base directory for relative paths.
129
- * @returns {NormalizedServerConfig} Normalized configuration.
130
- */
131
- export const normalizeServerConfig = (config, directoryPath = '.') => {
132
- if (!isObject(config)) {
133
- throw new Error('Конфигурация сервера должна содержать JSON-объект');
134
- }
135
-
136
- assertKnownKeys(config, CONFIG_KEYS, 'config');
137
-
138
- const database = normalizeDatabase(config.database, directoryPath);
139
- const files = normalizeFiles(config.files, directoryPath);
140
- const openapi = getObject(config.openapi, 'config.openapi') ?? {};
141
- const server = getObject(config.server, 'config.server') ?? {};
142
-
143
- assertKnownKeys(openapi, OPENAPI_KEYS, 'config.openapi');
144
- assertKnownKeys(server, SERVER_KEYS, 'config.server');
145
-
146
- const openapiPath = getString(openapi.path, 'config.openapi.path');
147
- const host = getString(server.host, 'config.server.host');
148
-
149
- if (server.port != null && (!Number.isInteger(server.port) || server.port < 0 || server.port > 65_535)) {
150
- throw new Error('Ключ config.server.port должен быть целым числом от 0 до 65535');
151
- }
152
-
153
- if (server.maxPageSize != null && (!Number.isInteger(server.maxPageSize) || server.maxPageSize < 1)) {
154
- throw new Error('Ключ config.server.maxPageSize должен быть положительным целым числом');
155
- }
156
-
157
- if (server.maxFileSize != null && (!Number.isInteger(server.maxFileSize) || server.maxFileSize < 1)) {
158
- throw new Error('Ключ config.server.maxFileSize должен быть положительным целым числом');
159
- }
160
-
161
- if (server.logger != null && typeof server.logger !== 'boolean' && !isObject(server.logger)) {
162
- throw new Error('Ключ config.server.logger должен содержать boolean или JSON-объект');
163
- }
164
-
165
- return {
166
- database,
167
- files,
168
- openapi: { path: resolveConfigPath(openapiPath, directoryPath) },
169
- server: {
170
- host,
171
- logger: server.logger,
172
- maxFileSize: server.maxFileSize,
173
- maxPageSize: server.maxPageSize,
174
- port: server.port,
175
- },
176
- };
177
- };
178
-
179
- /**
180
- * Loads an ES module config and resolves paths from its directory.
181
- * @param {string} configPath Configuration module path.
182
- * @returns {Promise<NormalizedServerConfig>} Normalized configuration.
183
- */
184
- export async function readServerConfig(configPath) {
185
- const resolvedConfigPath = resolve(getString(configPath, 'config', true));
186
- let config;
187
-
188
- try {
189
- const configUrl = pathToFileURL(resolvedConfigPath);
190
-
191
- // Bypass the module cache so repeated reads use the latest config.
192
- configUrl.searchParams.set('deep-json-server-import', String(configImportIndex++));
193
- config = (await import(configUrl.href)).default;
194
- } catch (error) {
195
- throw new Error(`Не удалось загрузить конфигурацию ${resolvedConfigPath}: ${error.message}`, { cause: error });
196
- }
197
-
198
- if (!isObject(config)) {
199
- throw new Error('Конфигурация сервера должна экспортировать JSON-объект через export default');
200
- }
201
-
202
- return normalizeServerConfig(config, dirname(resolvedConfigPath));
203
- }
package/src/database.js DELETED
@@ -1,148 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
- import { JSONFilePreset } from 'lowdb/node';
4
- import { createHttpError, createSerialQueue, createUniqueId, isObject, isSafeKey, resolveDatabasePath } from './utils.js';
5
-
6
- /** @typedef {{ data: import('./config.js').DatabaseData }} DatabaseContainer */
7
- /**
8
- * @typedef {object} DatabaseStore
9
- * @property {DatabaseContainer} database Current database container.
10
- * @property {string} [path] Resolved database file path.
11
- * @property {() => Promise<import('./config.js').DatabaseData>} read Returns current data and reloads disk-backed sources.
12
- * @property {<T>(operation: (database: DatabaseContainer) => T) => Promise<T>} update Runs a serialized update.
13
- */
14
-
15
- const RESOURCE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
16
-
17
- export const validateDatabase = (data) => {
18
- if (!isObject(data)) {
19
- throw new Error('База данных должна содержать JSON-объект');
20
- }
21
-
22
- Object.entries(data).forEach(([resource, records]) => {
23
- if (!RESOURCE_NAME_PATTERN.test(resource) || !isSafeKey(resource)) {
24
- throw new Error(`Недопустимое имя ресурса «${resource}»`);
25
- }
26
-
27
- if (!Array.isArray(records)) {
28
- throw new Error(`Ресурс «${resource}» должен содержать JSON-массив`);
29
- }
30
-
31
- const ids = new Set();
32
-
33
- records.forEach((record, index) => {
34
- if (!isObject(record)) {
35
- throw new Error(`Запись ${index} ресурса «${resource}» должна содержать JSON-объект`);
36
- }
37
-
38
- if (typeof record.id !== 'string' && !(typeof record.id === 'number' && Number.isFinite(record.id))) {
39
- throw new Error(`Запись ${index} ресурса «${resource}» должна содержать строковый или числовой id`);
40
- }
41
-
42
- if (String(record.id) === '') {
43
- throw new Error(`Запись ${index} ресурса «${resource}» должна содержать непустой id`);
44
- }
45
-
46
- const id = String(record.id);
47
-
48
- if (ids.has(id)) {
49
- throw new Error(`Ресурс «${resource}» содержит повторяющийся id «${id}»`);
50
- }
51
-
52
- ids.add(id);
53
- });
54
- });
55
-
56
- return data;
57
- };
58
-
59
- export const readJsonObject = async (path, label) => {
60
- let source;
61
-
62
- try {
63
- source = await readFile(resolve(path), 'utf8');
64
- } catch (error) {
65
- if (error?.code === 'ENOENT') {
66
- throw new Error(`${label} не найден: ${resolve(path)}`);
67
- }
68
-
69
- throw error;
70
- }
71
-
72
- const value = JSON.parse(source);
73
-
74
- if (!isObject(value)) {
75
- throw new Error(`${label} должен содержать JSON-объект`);
76
- }
77
-
78
- return value;
79
- };
80
-
81
- export const readDatabaseFile = async (databasePath) => validateDatabase(await readJsonObject(databasePath, 'Файл базы данных'));
82
-
83
- const createDiskDatabaseStore = async (databasePath) => {
84
- const resolvedDatabasePath = resolveDatabasePath(databasePath);
85
- const initialData = await readDatabaseFile(resolvedDatabasePath);
86
- const database = await JSONFilePreset(resolvedDatabasePath, initialData);
87
- const schedule = createSerialQueue();
88
-
89
- const read = async () => {
90
- database.data = await readDatabaseFile(resolvedDatabasePath);
91
-
92
- return database.data;
93
- };
94
-
95
- const update = (operation) =>
96
- schedule(async () => {
97
- await read();
98
-
99
- const result = operation(database);
100
-
101
- validateDatabase(database.data);
102
- await database.write();
103
-
104
- return result;
105
- });
106
-
107
- return { database, path: resolvedDatabasePath, read, update };
108
- };
109
-
110
- const createMemoryDatabaseStore = (sourceData) => {
111
- const database = { data: validateDatabase(structuredClone(sourceData)) };
112
- const schedule = createSerialQueue();
113
-
114
- const read = async () => database.data;
115
- const update = (operation) =>
116
- schedule(() => {
117
- const draft = { data: structuredClone(database.data) };
118
- const result = operation(draft);
119
-
120
- validateDatabase(draft.data);
121
- database.data = draft.data;
122
-
123
- return result;
124
- });
125
-
126
- return { database, read, update };
127
- };
128
-
129
- /**
130
- * Creates a disk- or memory-backed database with serialized updates.
131
- * @param {import('./config.js').DatabaseConfig} config Database source.
132
- * @returns {Promise<DatabaseStore>} Database store.
133
- */
134
- export const createDatabaseStore = async (config) => ('data' in config ? createMemoryDatabaseStore(config.data) : createDiskDatabaseStore(config.path));
135
-
136
- export const getCollection = (database, resource) => {
137
- const collection = isSafeKey(resource) ? database.data[resource] : undefined;
138
-
139
- if (!Array.isArray(collection)) {
140
- throw createHttpError(404, 'Ресурс не найден');
141
- }
142
-
143
- return collection;
144
- };
145
-
146
- export const findItem = (collection, id) => collection.find((item) => String(item.id) === String(id));
147
-
148
- export const createId = (collection) => createUniqueId((id) => findItem(collection, id) != null);