@kollors/deep-json-server 0.5.0 → 0.6.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.
@@ -1,25 +1,36 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { stringify } from 'yaml';
4
- import { readDatabaseFile } from '../database.js';
5
- import { readSchemaConfig } from './config.js';
6
- import { createOpenApiDocument } from './document.js';
7
-
8
- export { createOpenApiDocument } from './document.js';
9
-
10
- /**
11
- * Generates an OpenAPI YAML file.
12
- * @param {{ databasePath: string, files?: boolean, host?: string, outputPath: string, port?: number, schemaPath?: string }} options Generation options.
13
- * @returns {Promise<Record<string, unknown>>} Generated OpenAPI document.
14
- */
15
- export async function generateOpenApi({ databasePath, files = false, host, outputPath, port, schemaPath }) {
16
- const database = await readDatabaseFile(databasePath);
17
- const schemaConfig = await readSchemaConfig(schemaPath);
18
- const document = createOpenApiDocument(database, schemaConfig, { files, host, port });
4
+ import { DEFAULT_HOST, DEFAULT_PORT } from '../constants.js';
5
+ import { buildOpenapiDocument } from './document.js';
6
+
7
+ const getServerUrl = (host, port) => {
8
+ const serverPort = Number(port);
9
+
10
+ if (typeof host !== 'string' || host === '') {
11
+ throw new Error('Адрес сервера не должен быть пустым');
12
+ }
13
+
14
+ if (!Number.isInteger(serverPort) || serverPort < 1 || serverPort > 65_535) {
15
+ throw new Error('Порт должен быть целым числом от 1 до 65535');
16
+ }
17
+
18
+ const serverHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
19
+
20
+ return `http://${serverHost}:${serverPort}`;
21
+ };
22
+
23
+ export const createOpenapi = ({ database, files, host = DEFAULT_HOST, maxPageSize, port = DEFAULT_PORT, schema }) => {
24
+ const document = buildOpenapiDocument({ database, files, maxPageSize, schema });
25
+
26
+ document.servers = [{ url: getServerUrl(host, port) }];
27
+
28
+ return document;
29
+ };
30
+
31
+ export const writeOpenapi = async (document, outputPath) => {
19
32
  const resolvedOutputPath = resolve(outputPath);
20
33
 
21
34
  await mkdir(dirname(resolvedOutputPath), { recursive: true });
22
35
  await writeFile(resolvedOutputPath, stringify(document, { aliasDuplicateObjects: false, lineWidth: 0 }), 'utf8');
23
-
24
- return document;
25
- }
36
+ };
@@ -35,16 +35,10 @@ export const parsePagination = (query, maxPageSize = MAX_PAGE_SIZE) => {
35
35
  };
36
36
 
37
37
  export const paginateItems = (items, page, pageSize) => {
38
- const pages = Math.max(1, Math.ceil(items.length / pageSize));
39
38
  const offset = (page - 1) * pageSize;
40
39
 
41
40
  return {
42
41
  data: items.slice(offset, offset + pageSize),
43
- first: 1,
44
- items: items.length,
45
- last: pages,
46
- next: page < pages ? page + 1 : null,
47
- pages,
48
- prev: page > 1 ? Math.min(page - 1, pages) : null,
42
+ total: items.length,
49
43
  };
50
44
  };
package/src/server.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import Fastify from 'fastify';
2
+ import { normalizeServerConfig } from './config.js';
2
3
  import { DEFAULT_HOST, DEFAULT_MAX_FILE_SIZE, DEFAULT_PORT, MAX_PAGE_SIZE } from './constants.js';
3
4
  import { createDatabaseStore, createId, findItem, getCollection } from './database.js';
4
- import { registerFileRoutes } from './files.js';
5
- import { readSchemaConfig } from './openapi/config.js';
6
- import { createOpenApiDocument } from './openapi/index.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';
7
9
  import { matchesWhere, paginateItems, parsePagination, parseWhere, sortItems, validateWhere } from './query/index.js';
8
10
  import { embedItem, parseEmbedPaths, validateEmbedPaths } from './relations.js';
9
- import { createHttpError, getResourceNames, isObject, resolveDatabasePath } from './utils.js';
11
+ import { createHttpError, getResourceNames, isObject } from './utils.js';
12
+
13
+ /** @typedef {Record<string, unknown>} OpenapiDocument */
10
14
 
11
15
  const CORS_HEADERS = {
12
16
  'Access-Control-Allow-Headers': 'Content-Name, Content-Type',
@@ -142,97 +146,103 @@ const registerResourceRoutes = (server, store, resource, document, maxPageSize)
142
146
  };
143
147
 
144
148
  /**
145
- * Creates a Fastify server without opening a network port.
146
- * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, schemaPath?: string }} options Server options.
147
- * @returns {Promise<import('fastify').FastifyInstance>} Fastify server.
149
+ * Creates a Deep JSON Server facade.
150
+ * @param {import('./config.js').DeepJsonServerConfig} options Server options.
151
+ * @param {{ files?: boolean }} [features] Optional feature switches.
152
+ * @returns {Promise<{ fastify: () => import('fastify').FastifyInstance, openapi: () => Promise<OpenapiDocument> }>} Server facade.
148
153
  */
149
- export async function createServer(options) {
150
- const { databasePath, filesDirectoryPath, filesMetadataPath, logger = true, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxPageSize = MAX_PAGE_SIZE, schemaPath } = options ?? {};
151
-
152
- if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
153
- throw new Error('Максимальный размер страницы должен быть положительным целым числом');
154
+ export async function createServer(options, features = {}) {
155
+ const config = normalizeServerConfig(options);
156
+ const filesEnabled = features.files ?? config.files != null;
157
+ const { logger = true, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxPageSize = MAX_PAGE_SIZE } = config.server;
158
+ const store = await createDatabaseStore(config.database);
159
+ const schema = await resolveSchemaConfig(config.database.schema);
160
+ const fileStore = filesEnabled && config.files != null ? await createFileStore(config.files) : undefined;
161
+ let fastifyInstance;
162
+
163
+ if (filesEnabled && config.files == null) {
164
+ throw new Error('Для файловых маршрутов укажите секцию config.files');
154
165
  }
155
166
 
156
- if (!Number.isInteger(maxFileSize) || maxFileSize < 1) {
157
- throw new Error('Максимальный размер файла должен быть положительным целым числом');
158
- }
167
+ const buildDocument = () =>
168
+ buildOpenapiDocument({
169
+ database: store.database.data,
170
+ files: filesEnabled,
171
+ maxPageSize,
172
+ schema,
173
+ });
174
+
175
+ const fastify = () => {
176
+ if (fastifyInstance != null) {
177
+ return fastifyInstance;
178
+ }
179
+
180
+ const document = buildDocument();
181
+ const resources = getResourceNames(store.database.data);
182
+ const server = Fastify({ logger });
183
+ const listen = server.listen.bind(server);
159
184
 
160
- const store = await createDatabaseStore(databasePath);
161
- const schemaConfig = await readSchemaConfig(schemaPath);
162
- const document = createOpenApiDocument(store.database.data, schemaConfig);
163
- const resources = getResourceNames(store.database.data);
164
- const server = Fastify({ logger });
185
+ server.listen = (...args) => listen(...(args.length === 0 ? [{ host: config.server.host ?? DEFAULT_HOST, port: config.server.port ?? DEFAULT_PORT }] : args));
165
186
 
166
- addRequestSchemas(server, document, resources);
187
+ addRequestSchemas(server, document, resources);
167
188
 
168
- server.addHook('onRequest', async (_request, reply) => {
169
- Object.entries(CORS_HEADERS).forEach(([header, value]) => {
170
- reply.header(header, value);
189
+ server.addHook('onRequest', async (_request, reply) => {
190
+ Object.entries(CORS_HEADERS).forEach(([header, value]) => {
191
+ reply.header(header, value);
192
+ });
171
193
  });
172
- });
173
194
 
174
- server.addHook('preHandler', async (request) => {
175
- if (request.method === 'GET') {
176
- await store.read();
177
- }
178
- });
195
+ server.addHook('preHandler', async (request) => {
196
+ if (request.method === 'GET') {
197
+ await store.read();
198
+ }
199
+ });
179
200
 
180
- server.options('/', async (_request, reply) => reply.code(204).send());
181
- server.options('/*', async (_request, reply) => reply.code(204).send());
182
- server.get('/', async () => ({ resources }));
201
+ server.options('/', async (_request, reply) => reply.code(204).send());
202
+ server.options('/*', async (_request, reply) => reply.code(204).send());
203
+ server.get('/', async () => ({ resources }));
183
204
 
184
- resources.forEach((resource) => {
185
- registerResourceRoutes(server, store, resource, document, maxPageSize);
186
- });
205
+ resources.forEach((resource) => {
206
+ registerResourceRoutes(server, store, resource, document, maxPageSize);
207
+ });
187
208
 
188
- if ((filesDirectoryPath == null) !== (filesMetadataPath == null)) {
189
- throw new Error('Для файловых маршрутов укажите filesDirectoryPath и filesMetadataPath');
190
- }
209
+ if (fileStore != null) {
210
+ registerFileRoutes(server, { maxFileSize, store: fileStore });
211
+ }
191
212
 
192
- if (filesDirectoryPath != null) {
193
- await registerFileRoutes(server, { directoryPath: filesDirectoryPath, maxFileSize, metadataPath: filesMetadataPath });
194
- }
213
+ server.setErrorHandler((error, request, reply) => {
214
+ const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
195
215
 
196
- server.setErrorHandler((error, request, reply) => {
197
- const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
216
+ if (statusCode === 500) {
217
+ request.log.error(error);
218
+ }
198
219
 
199
- if (statusCode === 500) {
200
- request.log.error(error);
201
- }
220
+ return reply.code(statusCode).send({ error: error.message });
221
+ });
202
222
 
203
- return reply.code(statusCode).send({ error: error.message });
204
- });
223
+ fastifyInstance = server;
205
224
 
206
- return server;
207
- }
225
+ return fastifyInstance;
226
+ };
208
227
 
209
- /**
210
- * Creates and starts a Fastify server.
211
- * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, host?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, port?: number, schemaPath?: string }} options Server options.
212
- * @returns {Promise<import('fastify').FastifyInstance>} Listening Fastify server.
213
- */
214
- export async function startServer(options) {
215
- const {
216
- databasePath,
217
- filesDirectoryPath,
218
- filesMetadataPath,
219
- host = DEFAULT_HOST,
220
- logger = true,
221
- maxFileSize = DEFAULT_MAX_FILE_SIZE,
222
- maxPageSize = MAX_PAGE_SIZE,
223
- port = DEFAULT_PORT,
224
- schemaPath,
225
- } = options ?? {};
226
-
227
- if (!Number.isInteger(port) || port < 0 || port > 65_535) {
228
- throw new Error('Порт должен быть целым числом от 0 до 65535');
229
- }
228
+ const openapi = async () => {
229
+ await store.read();
230
230
 
231
- const resolvedDatabasePath = resolveDatabasePath(databasePath);
232
- const server = await createServer({ databasePath: resolvedDatabasePath, filesDirectoryPath, filesMetadataPath, logger, maxFileSize, maxPageSize, schemaPath });
231
+ const document = createOpenapi({
232
+ database: store.database.data,
233
+ files: filesEnabled,
234
+ host: config.server.host,
235
+ maxPageSize,
236
+ port: config.server.port,
237
+ schema,
238
+ });
239
+
240
+ if (config.openapi.path != null) {
241
+ await writeOpenapi(document, config.openapi.path);
242
+ }
233
243
 
234
- await server.listen({ host, port });
235
- server.log.info({ database: resolvedDatabasePath }, 'Deep JSON Server запущен');
244
+ return document;
245
+ };
236
246
 
237
- return server;
247
+ return { fastify, openapi };
238
248
  }
package/types/index.d.ts CHANGED
@@ -1,2 +1,11 @@
1
- export { createOpenApiDocument, generateOpenApi } from './src/openapi/index.js';
2
- export { createServer, startServer } from './src/server.js';
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/server.js').OpenapiDocument} OpenapiDocument */
6
+ export type DeepJsonServerConfig = import('./src/config.js').DeepJsonServerConfig;
7
+ export type DatabaseConfig = import('./src/config.js').DatabaseConfig;
8
+ export type FilesConfig = import('./src/config.js').FilesConfig;
9
+ export type MemoryFile = import('./src/config.js').MemoryFile;
10
+ export type OpenapiDocument = import('./src/server.js').OpenapiDocument;
11
+ export { createServer } from './src/server.js';
@@ -0,0 +1,86 @@
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
+ id: 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 DeepJsonServerConfig = {
28
+ /**
29
+ * Database source and optional schema.
30
+ */
31
+ database: DatabaseConfig;
32
+ /**
33
+ * Binary-file storage.
34
+ */
35
+ files?: FilesConfig;
36
+ /**
37
+ * Generated OpenAPI file.
38
+ */
39
+ openapi?: {
40
+ path?: string;
41
+ };
42
+ /**
43
+ * Runtime settings.
44
+ */
45
+ server?: {
46
+ host?: string;
47
+ logger?: boolean | Record<string, unknown>;
48
+ maxFileSize?: number;
49
+ maxPageSize?: number;
50
+ port?: number;
51
+ };
52
+ };
53
+ /**
54
+ * @param {DeepJsonServerConfig} config Server configuration.
55
+ * @param {string} [directoryPath] Base directory for relative paths.
56
+ * @returns {{ database: DatabaseConfig, files?: FilesConfig, openapi: { path?: string }, server: { host?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, port?: number } }} Normalized configuration.
57
+ */
58
+ export declare const normalizeServerConfig: (config: DeepJsonServerConfig, directoryPath?: string) => {
59
+ database: DatabaseConfig;
60
+ files?: FilesConfig;
61
+ openapi: {
62
+ path?: string;
63
+ };
64
+ server: {
65
+ host?: string;
66
+ logger?: boolean | Record<string, unknown>;
67
+ maxFileSize?: number;
68
+ maxPageSize?: number;
69
+ port?: number;
70
+ };
71
+ };
72
+ /** @param {string} configPath Configuration module path. */
73
+ export declare function readServerConfig(configPath: string): Promise<{
74
+ database: DatabaseConfig;
75
+ files?: FilesConfig;
76
+ openapi: {
77
+ path?: string;
78
+ };
79
+ server: {
80
+ host?: string;
81
+ logger?: boolean | Record<string, unknown>;
82
+ maxFileSize?: number;
83
+ maxPageSize?: number;
84
+ port?: number;
85
+ };
86
+ }>;
@@ -1,8 +1,11 @@
1
1
  export declare const validateDatabase: (data: any) => any;
2
2
  export declare const readJsonObject: (path: any, label: any) => Promise<any>;
3
3
  export declare const readDatabaseFile: (databasePath: any) => Promise<any>;
4
- /** @returns {Promise<any>} Internal LowDB-backed store. */
5
- export declare const createDatabaseStore: (databasePath: any) => Promise<any>;
4
+ /**
5
+ * @param {import('./config.js').DatabaseConfig} config Database source.
6
+ * @returns {Promise<any>} Internal database store.
7
+ */
8
+ export declare const createDatabaseStore: (config: import('./config.js').DatabaseConfig) => Promise<any>;
6
9
  export declare const getCollection: (database: any, resource: any) => any[];
7
10
  export declare const findItem: (collection: any, id: any) => any;
8
11
  export declare const createId: (collection: any) => any;
@@ -1,5 +1,24 @@
1
- export declare const registerFileRoutes: (server: any, { directoryPath, maxFileSize, metadataPath }: {
2
- directoryPath: any;
1
+ /** @param {import('./config.js').FilesConfig} config File storage configuration. */
2
+ export declare const createFileStore: (config: import('./config.js').FilesConfig) => Promise<{
3
+ get: (id: any) => Promise<void>;
4
+ remove: (id: any) => Promise<void>;
5
+ upload: ({ maxFileSize, mimeType, name, stream }: {
6
+ maxFileSize: any;
7
+ mimeType: any;
8
+ name: any;
9
+ stream: any;
10
+ }) => Promise<void>;
11
+ } | {
12
+ get: (id: any) => Promise<void>;
13
+ remove: (id: any) => Promise<void>;
14
+ upload: ({ maxFileSize, mimeType, name, stream }: {
15
+ maxFileSize: any;
16
+ mimeType: any;
17
+ name: any;
18
+ stream: any;
19
+ }) => Promise<void>;
20
+ }>;
21
+ export declare const registerFileRoutes: (server: any, { maxFileSize, store }: {
3
22
  maxFileSize: any;
4
- metadataPath: any;
5
- }) => Promise<void>;
23
+ store: any;
24
+ }) => void;
@@ -1,3 +1,3 @@
1
1
  export declare const validateSchemaConfig: (schemaConfig: any, resources: any) => any;
2
2
  export declare const applyConfiguredFields: (schema: any, resource: any, resourceConfig: any) => any;
3
- export declare const readSchemaConfig: (schemaPath: any) => Promise<any>;
3
+ export declare const resolveSchemaConfig: (schema: any) => Promise<any>;
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Creates an OpenAPI 3.0 document from a database and optional schema configuration.
3
- * @param {Record<string, Array<Record<string, unknown>>>} database Database contents.
4
- * @param {Record<string, unknown>} [schemaConfig] Schema configuration.
5
- * @param {{ files?: boolean, host?: string, port?: number }} [serverOptions] Server address and optional features used in the generated document.
2
+ * Builds an OpenAPI 3.0 document from resolved server data.
3
+ * @param {{ database: Record<string, Array<Record<string, unknown>>>, files?: boolean, maxPageSize?: number, schema?: Record<string, unknown> }} options Document options.
6
4
  * @returns {Record<string, unknown>} OpenAPI document.
7
5
  */
8
- export declare function createOpenApiDocument(database: Record<string, Array<Record<string, unknown>>>, schemaConfig?: Record<string, unknown>, { files, host, port }?: {
6
+ export declare function buildOpenapiDocument(options: {
7
+ database: Record<string, Array<Record<string, unknown>>>;
9
8
  files?: boolean;
10
- host?: string;
11
- port?: number;
9
+ maxPageSize?: number;
10
+ schema?: Record<string, unknown>;
12
11
  }): Record<string, unknown>;
@@ -1,14 +1,9 @@
1
- export { createOpenApiDocument } from './document.js';
2
- /**
3
- * Generates an OpenAPI YAML file.
4
- * @param {{ databasePath: string, files?: boolean, host?: string, outputPath: string, port?: number, schemaPath?: string }} options Generation options.
5
- * @returns {Promise<Record<string, unknown>>} Generated OpenAPI document.
6
- */
7
- export declare function generateOpenApi({ databasePath, files, host, outputPath, port, schemaPath }: {
8
- databasePath: string;
9
- files?: boolean;
10
- host?: string;
11
- outputPath: string;
12
- port?: number;
13
- schemaPath?: string;
14
- }): Promise<Record<string, unknown>>;
1
+ export declare const createOpenapi: ({ database, files, host, maxPageSize, port, schema }: {
2
+ database: any;
3
+ files: any;
4
+ host?: string | undefined;
5
+ maxPageSize: any;
6
+ port?: number | undefined;
7
+ schema: any;
8
+ }) => Record<string, unknown>;
9
+ export declare const writeOpenapi: (document: any, outputPath: any) => Promise<void>;
@@ -4,10 +4,5 @@ export declare const parsePagination: (query: any, maxPageSize?: number) => {
4
4
  };
5
5
  export declare const paginateItems: (items: any, page: any, pageSize: any) => {
6
6
  data: any;
7
- first: number;
8
- items: any;
9
- last: number;
10
- next: any;
11
- pages: number;
12
- prev: number | null;
7
+ total: any;
13
8
  };
@@ -1,30 +1,13 @@
1
+ export type OpenapiDocument = Record<string, unknown>;
1
2
  /**
2
- * Creates a Fastify server without opening a network port.
3
- * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, schemaPath?: string }} options Server options.
4
- * @returns {Promise<import('fastify').FastifyInstance>} Fastify server.
3
+ * Creates a Deep JSON Server facade.
4
+ * @param {import('./config.js').DeepJsonServerConfig} options Server options.
5
+ * @param {{ files?: boolean }} [features] Optional feature switches.
6
+ * @returns {Promise<{ fastify: () => import('fastify').FastifyInstance, openapi: () => Promise<OpenapiDocument> }>} Server facade.
5
7
  */
6
- export declare function createServer(options: {
7
- databasePath: string;
8
- filesDirectoryPath?: string;
9
- filesMetadataPath?: string;
10
- logger?: boolean | Record<string, unknown>;
11
- maxFileSize?: number;
12
- maxPageSize?: number;
13
- schemaPath?: string;
14
- }): Promise<import('fastify').FastifyInstance>;
15
- /**
16
- * Creates and starts a Fastify server.
17
- * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, host?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, port?: number, schemaPath?: string }} options Server options.
18
- * @returns {Promise<import('fastify').FastifyInstance>} Listening Fastify server.
19
- */
20
- export declare function startServer(options: {
21
- databasePath: string;
22
- filesDirectoryPath?: string;
23
- filesMetadataPath?: string;
24
- host?: string;
25
- logger?: boolean | Record<string, unknown>;
26
- maxFileSize?: number;
27
- maxPageSize?: number;
28
- port?: number;
29
- schemaPath?: string;
30
- }): Promise<import('fastify').FastifyInstance>;
8
+ export declare function createServer(options: import('./config.js').DeepJsonServerConfig, features?: {
9
+ files?: boolean;
10
+ }): Promise<{
11
+ fastify: () => import('fastify').FastifyInstance;
12
+ openapi: () => Promise<OpenapiDocument>;
13
+ }>;