@kollors/deep-json-server 0.6.0 → 0.7.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 +41 -12
- package/README.ru.md +41 -12
- package/index.js +5 -0
- package/package.json +9 -2
- package/src/cli.js +3 -3
- package/src/config.js +14 -5
- package/src/constants.js +1 -1
- package/src/database.js +19 -28
- package/src/files.js +394 -129
- package/src/openapi/document.js +109 -32
- package/src/query/filter.js +11 -10
- package/src/query/pagination.js +2 -2
- package/src/relation-metadata.js +1 -0
- package/src/server.js +47 -45
- package/src/utils.js +30 -0
- package/types/index.d.ts +10 -0
- package/types/src/config.d.ts +28 -41
- package/types/src/constants.d.ts +1 -1
- package/types/src/database.d.ts +25 -3
- package/types/src/files.d.ts +61 -21
- package/types/src/openapi/document.d.ts +2 -2
- package/types/src/server.d.ts +9 -8
- package/types/src/utils.d.ts +8 -0
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ export default {
|
|
|
110
110
|
schema: { $info: { title: 'Movie API', version: '1.0.0' } },
|
|
111
111
|
},
|
|
112
112
|
files: {
|
|
113
|
-
data: [{ content: new Uint8Array([1, 2, 3]),
|
|
113
|
+
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
116
|
```
|
|
@@ -352,33 +352,62 @@ Add `files.directory` and `files.metadata` to the server config, then pass `--fi
|
|
|
352
352
|
deep-json-server --files server.config.js
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
-
For temporary tests, use `files.data` instead. Each initial record contains `
|
|
355
|
+
For temporary tests, use `files.data` instead. Each initial record contains `name`, `mimeType`, binary `content` as a `Uint8Array`, and an optional `directory`. Uploaded files then remain in memory until the process exits.
|
|
356
356
|
|
|
357
|
-
Upload one file as the request body.
|
|
357
|
+
Upload one file directly as the request body. `Content-Name` contains the URI-encoded file name, `Content-Type` contains its MIME type, and the optional `Content-Directory` contains the URI-encoded relative directory:
|
|
358
358
|
|
|
359
359
|
```http
|
|
360
|
-
POST /_files
|
|
361
|
-
Content-Name:
|
|
360
|
+
POST /_files/storage
|
|
361
|
+
Content-Name: shadows-of-ardenia.jpg
|
|
362
|
+
Content-Directory: posters
|
|
362
363
|
Content-Type: image/jpeg
|
|
363
364
|
|
|
364
365
|
<binary body>
|
|
365
366
|
```
|
|
366
367
|
|
|
367
|
-
A
|
|
368
|
+
A new file returns status `201` and its computed metadata:
|
|
368
369
|
|
|
369
370
|
```json
|
|
370
371
|
{
|
|
371
|
-
"
|
|
372
|
+
"directory": "posters",
|
|
373
|
+
"downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
|
|
374
|
+
"metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
|
|
372
375
|
"mimeType": "image/jpeg",
|
|
373
|
-
"name": "
|
|
376
|
+
"name": "shadows-of-ardenia.jpg",
|
|
374
377
|
"size": 182340,
|
|
375
|
-
"url": "/_files/
|
|
378
|
+
"url": "/_files/storage/posters/shadows-of-ardenia.jpg"
|
|
376
379
|
}
|
|
377
380
|
```
|
|
378
381
|
|
|
379
|
-
|
|
382
|
+
The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
|
|
380
383
|
|
|
381
|
-
|
|
384
|
+
```text
|
|
385
|
+
POST /_files/storage Upload or replace a file
|
|
386
|
+
GET /_files/storage/* Return file contents inline
|
|
387
|
+
PATCH /_files/storage/* Rename or move a file
|
|
388
|
+
DELETE /_files/storage/* Delete a file
|
|
389
|
+
|
|
390
|
+
GET /_files/metadata/* Return file metadata as JSON
|
|
391
|
+
GET /_files/download/* Download a file as an attachment
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Rename, move, or perform both operations with a JSON body. At least one field is required:
|
|
395
|
+
|
|
396
|
+
```http
|
|
397
|
+
PATCH /_files/storage/posters/shadows-of-ardenia.jpg
|
|
398
|
+
Content-Type: application/json
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
"directory": "archive/posters",
|
|
402
|
+
"name": "ardenia-shadows.jpg"
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`PATCH` returns the updated metadata with status `200`; a conflicting destination returns `409`. `DELETE` returns `204` without a response body. A missing file returns `404` on every path-based operation. File paths in URLs are relative to `files.directory`, and all returned URLs are relative to the mock-server origin.
|
|
407
|
+
|
|
408
|
+
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. The metadata file contains only `directory`, `mimeType`, and `name`; `size` is read from the actual file, while response URLs are computed. The server creates directories automatically. The metadata file may be absent initially and is created on the first upload. Do not edit stored files or metadata while the server is running. Metadata created by versions before this path-based API is not compatible with the new format.
|
|
409
|
+
|
|
410
|
+
The upload is raw binary rather than `multipart/form-data`, so `XMLHttpRequest.upload.onprogress` can report progress while the browser sends a `File` directly with `xhr.send(file)`. The default maximum size is 100 MiB and can be changed through `server.maxFileSize`. Missing or unsafe headers and paths return `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
|
|
382
411
|
|
|
383
412
|
## Database schema and OpenAPI generation
|
|
384
413
|
|
|
@@ -522,7 +551,7 @@ const memoryServer = await createServer({
|
|
|
522
551
|
schema: { $info: { title: 'Movie API', version: '1.0.0' } },
|
|
523
552
|
},
|
|
524
553
|
files: {
|
|
525
|
-
data: [{ content: new Uint8Array([1, 2, 3]),
|
|
554
|
+
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
526
555
|
},
|
|
527
556
|
});
|
|
528
557
|
|
package/README.ru.md
CHANGED
|
@@ -110,7 +110,7 @@ export default {
|
|
|
110
110
|
schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
|
|
111
111
|
},
|
|
112
112
|
files: {
|
|
113
|
-
data: [{ content: new Uint8Array([1, 2, 3]),
|
|
113
|
+
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
116
|
```
|
|
@@ -352,33 +352,62 @@ GET /countries/1?_embed=users
|
|
|
352
352
|
deep-json-server --files server.config.js
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
-
Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `
|
|
355
|
+
Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
|
|
356
356
|
|
|
357
|
-
Один файл отправляется непосредственно в теле запроса.
|
|
357
|
+
Один файл отправляется непосредственно в теле запроса. `Content-Name` содержит URI-кодированное имя файла, `Content-Type` — его MIME-тип, а необязательный `Content-Directory` — URI-кодированный относительный путь к директории:
|
|
358
358
|
|
|
359
359
|
```http
|
|
360
|
-
POST /_files
|
|
361
|
-
Content-Name:
|
|
360
|
+
POST /_files/storage
|
|
361
|
+
Content-Name: shadows-of-ardenia.jpg
|
|
362
|
+
Content-Directory: posters
|
|
362
363
|
Content-Type: image/jpeg
|
|
363
364
|
|
|
364
365
|
<binary body>
|
|
365
366
|
```
|
|
366
367
|
|
|
367
|
-
|
|
368
|
+
Новый файл возвращает статус `201` и вычисленные метаданные:
|
|
368
369
|
|
|
369
370
|
```json
|
|
370
371
|
{
|
|
371
|
-
"
|
|
372
|
+
"directory": "posters",
|
|
373
|
+
"downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
|
|
374
|
+
"metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
|
|
372
375
|
"mimeType": "image/jpeg",
|
|
373
|
-
"name": "
|
|
376
|
+
"name": "shadows-of-ardenia.jpg",
|
|
374
377
|
"size": 182340,
|
|
375
|
-
"url": "/_files/
|
|
378
|
+
"url": "/_files/storage/posters/shadows-of-ardenia.jpg"
|
|
376
379
|
}
|
|
377
380
|
```
|
|
378
381
|
|
|
379
|
-
|
|
382
|
+
Сочетание `directory` и `name` идентифицирует файл. Повторная загрузка по существующему пути возвращает `409`. Чтобы заменить файл, передайте `Content-Override: true`; успешная перезапись возвращает `200`. Сервер поддерживает следующие файловые маршруты:
|
|
380
383
|
|
|
381
|
-
|
|
384
|
+
```text
|
|
385
|
+
POST /_files/storage Загрузка или перезапись файла
|
|
386
|
+
GET /_files/storage/* Просмотр содержимого файла
|
|
387
|
+
PATCH /_files/storage/* Переименование или перемещение файла
|
|
388
|
+
DELETE /_files/storage/* Удаление файла
|
|
389
|
+
|
|
390
|
+
GET /_files/metadata/* Получение метаданных в JSON
|
|
391
|
+
GET /_files/download/* Скачивание файла
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Для переименования, перемещения либо обеих операций отправьте JSON-объект. Нужно указать хотя бы одно поле:
|
|
395
|
+
|
|
396
|
+
```http
|
|
397
|
+
PATCH /_files/storage/posters/shadows-of-ardenia.jpg
|
|
398
|
+
Content-Type: application/json
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
"directory": "archive/posters",
|
|
402
|
+
"name": "ardenia-shadows.jpg"
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`PATCH` возвращает обновлённые метаданные со статусом `200`; занятый конечный путь возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса mock-сервера.
|
|
407
|
+
|
|
408
|
+
При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Сервер автоматически создаёт директории. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке. Не редактируйте сохранённые файлы или метаданные во время работы сервера. Метаданные, созданные версиями до перехода на адресацию по пути, несовместимы с новым форматом.
|
|
409
|
+
|
|
410
|
+
Используется бинарное тело запроса, а не `multipart/form-data`, поэтому `XMLHttpRequest.upload.onprogress` может показывать прогресс при непосредственной отправке `File` через `xhr.send(file)`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
382
411
|
|
|
383
412
|
## Схема базы данных и генерация OpenAPI
|
|
384
413
|
|
|
@@ -522,7 +551,7 @@ const memoryServer = await createServer({
|
|
|
522
551
|
schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
|
|
523
552
|
},
|
|
524
553
|
files: {
|
|
525
|
-
data: [{ content: new Uint8Array([1, 2, 3]),
|
|
554
|
+
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
526
555
|
},
|
|
527
556
|
});
|
|
528
557
|
|
package/index.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
/** @typedef {import('./src/config.js').DatabaseConfig} DatabaseConfig */
|
|
3
3
|
/** @typedef {import('./src/config.js').FilesConfig} FilesConfig */
|
|
4
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 */
|
|
5
9
|
/** @typedef {import('./src/server.js').OpenapiDocument} OpenapiDocument */
|
|
10
|
+
/** @typedef {import('./src/server.js').ServerFacade} ServerFacade */
|
|
6
11
|
|
|
7
12
|
export { createServer } from './src/server.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kollors/deep-json-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "JSON mock server with deep filters and recursive relationship embedding",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -52,11 +52,18 @@
|
|
|
52
52
|
},
|
|
53
53
|
"keywords": [
|
|
54
54
|
"api",
|
|
55
|
+
"crud",
|
|
55
56
|
"deep-filter",
|
|
57
|
+
"file-upload",
|
|
56
58
|
"json",
|
|
59
|
+
"json-server",
|
|
57
60
|
"mock",
|
|
61
|
+
"mock-api",
|
|
62
|
+
"mock-server",
|
|
63
|
+
"openapi",
|
|
58
64
|
"relations",
|
|
59
|
-
"rest"
|
|
65
|
+
"rest",
|
|
66
|
+
"rest-api"
|
|
60
67
|
],
|
|
61
68
|
"author": "kollors",
|
|
62
69
|
"license": "MIT",
|
package/src/cli.js
CHANGED
|
@@ -73,10 +73,10 @@ export async function runCli(args = process.argv.slice(2), services = { createSe
|
|
|
73
73
|
validateModeConfig(config, { files, openapiMode });
|
|
74
74
|
|
|
75
75
|
const runtimeConfig = { ...config, server: { ...config.server, host, port } };
|
|
76
|
-
const
|
|
76
|
+
const serverFacade = await services.createServer(runtimeConfig, { files });
|
|
77
77
|
|
|
78
78
|
if (openapiMode !== 'none') {
|
|
79
|
-
await
|
|
79
|
+
await serverFacade.openapi();
|
|
80
80
|
process.stdout.write(`OpenAPI-схема сохранена в ${config.openapi.path}\n`);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -84,7 +84,7 @@ export async function runCli(args = process.argv.slice(2), services = { createSe
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const fastify =
|
|
87
|
+
const fastify = serverFacade.fastify();
|
|
88
88
|
|
|
89
89
|
await fastify.listen({ host, port });
|
|
90
90
|
fastify.log.info({ database: 'path' in config.database ? config.database.path : 'memory' }, 'Deep JSON Server запущен');
|
package/src/config.js
CHANGED
|
@@ -12,15 +12,18 @@ let configImportIndex = 0;
|
|
|
12
12
|
/** @typedef {Record<string, Array<Record<string, unknown>>>} DatabaseData */
|
|
13
13
|
/** @typedef {Record<string, unknown>} DatabaseSchema */
|
|
14
14
|
/** @typedef {{ data: DatabaseData, path?: never, schema?: DatabaseSchema | string } | { data?: never, path: string, schema?: DatabaseSchema | string }} DatabaseConfig */
|
|
15
|
-
/** @typedef {{ content: Uint8Array,
|
|
15
|
+
/** @typedef {{ content: Uint8Array, directory?: string, mimeType: string, name: string }} MemoryFile */
|
|
16
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 */
|
|
17
19
|
/**
|
|
18
20
|
* @typedef {object} DeepJsonServerConfig
|
|
19
21
|
* @property {DatabaseConfig} database Database source and optional schema.
|
|
20
22
|
* @property {FilesConfig} [files] Binary-file storage.
|
|
21
|
-
* @property {
|
|
22
|
-
* @property {
|
|
23
|
+
* @property {OpenapiConfig} [openapi] Generated OpenAPI file.
|
|
24
|
+
* @property {ServerConfig} [server] Runtime settings.
|
|
23
25
|
*/
|
|
26
|
+
/** @typedef {{ database: DatabaseConfig, files?: FilesConfig, openapi: OpenapiConfig, server: ServerConfig }} NormalizedServerConfig */
|
|
24
27
|
|
|
25
28
|
const assertKnownKeys = (value, keys, path) => {
|
|
26
29
|
const unknownKey = Object.keys(value).find((key) => !keys.has(key));
|
|
@@ -120,9 +123,10 @@ const normalizeFiles = (value, directoryPath) => {
|
|
|
120
123
|
};
|
|
121
124
|
|
|
122
125
|
/**
|
|
126
|
+
* Validates configuration and resolves relative paths.
|
|
123
127
|
* @param {DeepJsonServerConfig} config Server configuration.
|
|
124
128
|
* @param {string} [directoryPath] Base directory for relative paths.
|
|
125
|
-
* @returns {
|
|
129
|
+
* @returns {NormalizedServerConfig} Normalized configuration.
|
|
126
130
|
*/
|
|
127
131
|
export const normalizeServerConfig = (config, directoryPath = '.') => {
|
|
128
132
|
if (!isObject(config)) {
|
|
@@ -172,7 +176,11 @@ export const normalizeServerConfig = (config, directoryPath = '.') => {
|
|
|
172
176
|
};
|
|
173
177
|
};
|
|
174
178
|
|
|
175
|
-
/**
|
|
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
|
+
*/
|
|
176
184
|
export async function readServerConfig(configPath) {
|
|
177
185
|
const resolvedConfigPath = resolve(getString(configPath, 'config', true));
|
|
178
186
|
let config;
|
|
@@ -180,6 +188,7 @@ export async function readServerConfig(configPath) {
|
|
|
180
188
|
try {
|
|
181
189
|
const configUrl = pathToFileURL(resolvedConfigPath);
|
|
182
190
|
|
|
191
|
+
// Bypass the module cache so repeated reads use the latest config.
|
|
183
192
|
configUrl.searchParams.set('deep-json-server-import', String(configImportIndex++));
|
|
184
193
|
config = (await import(configUrl.href)).default;
|
|
185
194
|
} catch (error) {
|
package/src/constants.js
CHANGED
package/src/database.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
import { readFile } from 'node:fs/promises';
|
|
3
2
|
import { resolve } from 'node:path';
|
|
4
3
|
import { JSONFilePreset } from 'lowdb/node';
|
|
5
|
-
import { createHttpError, isObject, isSafeKey, resolveDatabasePath } from './utils.js';
|
|
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
|
+
*/
|
|
6
14
|
|
|
7
15
|
const RESOURCE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
8
16
|
|
|
@@ -76,7 +84,7 @@ const createDiskDatabaseStore = async (databasePath) => {
|
|
|
76
84
|
const resolvedDatabasePath = resolveDatabasePath(databasePath);
|
|
77
85
|
const initialData = await readDatabaseFile(resolvedDatabasePath);
|
|
78
86
|
const database = await JSONFilePreset(resolvedDatabasePath, initialData);
|
|
79
|
-
|
|
87
|
+
const schedule = createSerialQueue();
|
|
80
88
|
|
|
81
89
|
const read = async () => {
|
|
82
90
|
database.data = await readDatabaseFile(resolvedDatabasePath);
|
|
@@ -84,8 +92,8 @@ const createDiskDatabaseStore = async (databasePath) => {
|
|
|
84
92
|
return database.data;
|
|
85
93
|
};
|
|
86
94
|
|
|
87
|
-
const update = (operation) =>
|
|
88
|
-
|
|
95
|
+
const update = (operation) =>
|
|
96
|
+
schedule(async () => {
|
|
89
97
|
await read();
|
|
90
98
|
|
|
91
99
|
const result = operation(database);
|
|
@@ -96,21 +104,16 @@ const createDiskDatabaseStore = async (databasePath) => {
|
|
|
96
104
|
return result;
|
|
97
105
|
});
|
|
98
106
|
|
|
99
|
-
writeQueue = pendingOperation.catch(() => undefined);
|
|
100
|
-
|
|
101
|
-
return pendingOperation;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
107
|
return { database, path: resolvedDatabasePath, read, update };
|
|
105
108
|
};
|
|
106
109
|
|
|
107
110
|
const createMemoryDatabaseStore = (sourceData) => {
|
|
108
111
|
const database = { data: validateDatabase(structuredClone(sourceData)) };
|
|
109
|
-
|
|
112
|
+
const schedule = createSerialQueue();
|
|
110
113
|
|
|
111
114
|
const read = async () => database.data;
|
|
112
|
-
const update = (operation) =>
|
|
113
|
-
|
|
115
|
+
const update = (operation) =>
|
|
116
|
+
schedule(() => {
|
|
114
117
|
const draft = { data: structuredClone(database.data) };
|
|
115
118
|
const result = operation(draft);
|
|
116
119
|
|
|
@@ -120,17 +123,13 @@ const createMemoryDatabaseStore = (sourceData) => {
|
|
|
120
123
|
return result;
|
|
121
124
|
});
|
|
122
125
|
|
|
123
|
-
updateQueue = pendingOperation.catch(() => undefined);
|
|
124
|
-
|
|
125
|
-
return pendingOperation;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
126
|
return { database, read, update };
|
|
129
127
|
};
|
|
130
128
|
|
|
131
129
|
/**
|
|
130
|
+
* Creates a disk- or memory-backed database with serialized updates.
|
|
132
131
|
* @param {import('./config.js').DatabaseConfig} config Database source.
|
|
133
|
-
* @returns {Promise<
|
|
132
|
+
* @returns {Promise<DatabaseStore>} Database store.
|
|
134
133
|
*/
|
|
135
134
|
export const createDatabaseStore = async (config) => ('data' in config ? createMemoryDatabaseStore(config.data) : createDiskDatabaseStore(config.path));
|
|
136
135
|
|
|
@@ -146,12 +145,4 @@ export const getCollection = (database, resource) => {
|
|
|
146
145
|
|
|
147
146
|
export const findItem = (collection, id) => collection.find((item) => String(item.id) === String(id));
|
|
148
147
|
|
|
149
|
-
export const createId = (collection) =>
|
|
150
|
-
let id;
|
|
151
|
-
|
|
152
|
-
do {
|
|
153
|
-
id = randomBytes(8).toString('base64url');
|
|
154
|
-
} while (findItem(collection, id) != null);
|
|
155
|
-
|
|
156
|
-
return id;
|
|
157
|
-
};
|
|
148
|
+
export const createId = (collection) => createUniqueId((id) => findItem(collection, id) != null);
|