@kollors/deep-json-server 0.4.0 → 0.5.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/src/files.js ADDED
@@ -0,0 +1,278 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ import { Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { createHttpError, isObject } from './utils.js';
8
+
9
+ const createFileId = (files) => {
10
+ let id;
11
+
12
+ do {
13
+ id = randomBytes(8).toString('base64url');
14
+ } while (files.some((file) => file.id === id));
15
+
16
+ return id;
17
+ };
18
+
19
+ const validateFileMetadata = (files, metadataPath) => {
20
+ if (!Array.isArray(files)) {
21
+ throw new Error(`Файл метаданных ${metadataPath} должен содержать JSON-массив`);
22
+ }
23
+
24
+ const ids = new Set();
25
+
26
+ files.forEach((file, index) => {
27
+ if (
28
+ !isObject(file) ||
29
+ typeof file.id !== 'string' ||
30
+ file.id === '' ||
31
+ typeof file.mimeType !== 'string' ||
32
+ file.mimeType === '' ||
33
+ typeof file.name !== 'string' ||
34
+ file.name === '' ||
35
+ !Number.isInteger(file.size) ||
36
+ file.size < 0 ||
37
+ file.url !== `/_files/${file.id}`
38
+ ) {
39
+ throw new Error(`Некорректная запись ${index} в файле метаданных ${metadataPath}`);
40
+ }
41
+
42
+ if (ids.has(file.id)) {
43
+ throw new Error(`Файл метаданных ${metadataPath} содержит повторяющийся id «${file.id}»`);
44
+ }
45
+
46
+ ids.add(file.id);
47
+ });
48
+
49
+ return files;
50
+ };
51
+
52
+ const readMetadata = async (metadataPath) => {
53
+ try {
54
+ return validateFileMetadata(JSON.parse(await readFile(metadataPath, 'utf8')), metadataPath);
55
+ } catch (error) {
56
+ if (error?.code === 'ENOENT') {
57
+ return [];
58
+ }
59
+
60
+ throw error;
61
+ }
62
+ };
63
+
64
+ const writeMetadata = async (metadataPath, files) => {
65
+ const temporaryPath = `${metadataPath}.${randomBytes(6).toString('hex')}.tmp`;
66
+
67
+ try {
68
+ await writeFile(temporaryPath, JSON.stringify(files, null, 2), { encoding: 'utf8', flag: 'wx' });
69
+ await rename(temporaryPath, metadataPath);
70
+ } catch (error) {
71
+ await rm(temporaryPath, { force: true });
72
+ throw error;
73
+ }
74
+ };
75
+
76
+ const getContentName = (value) => {
77
+ if (typeof value !== 'string' || value === '') {
78
+ throw createHttpError(400, 'Заголовок Content-Name обязателен');
79
+ }
80
+
81
+ let name;
82
+
83
+ try {
84
+ name = decodeURIComponent(value).replaceAll('\\', '/');
85
+ } catch {
86
+ throw createHttpError(400, 'Заголовок Content-Name содержит некорректное значение');
87
+ }
88
+
89
+ const parts = name.split('/');
90
+
91
+ if (name.startsWith('/') || parts.some((part) => part === '' || part === '.' || part === '..' || part.includes('\0'))) {
92
+ throw createHttpError(400, 'Заголовок Content-Name должен содержать безопасный относительный путь');
93
+ }
94
+
95
+ return name;
96
+ };
97
+
98
+ const getMimeType = (value) => {
99
+ const mimeType = typeof value === 'string' ? value.split(';', 1)[0].trim().toLowerCase() : '';
100
+
101
+ if (mimeType === '' || !/^[\w!#$&^.+-]+\/[\w!#$&^.+-]+$/.test(mimeType)) {
102
+ throw createHttpError(400, 'Заголовок Content-Type должен содержать корректный MIME-тип');
103
+ }
104
+
105
+ return mimeType;
106
+ };
107
+
108
+ const createSizeLimiter = (maxFileSize, onSize) => {
109
+ let size = 0;
110
+
111
+ return new Transform({
112
+ transform(chunk, _encoding, callback) {
113
+ size += chunk.length;
114
+
115
+ if (size > maxFileSize) {
116
+ callback(createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`));
117
+ return;
118
+ }
119
+
120
+ onSize(size);
121
+ callback(null, chunk);
122
+ },
123
+ });
124
+ };
125
+
126
+ const createFileStore = async ({ directoryPath: sourceDirectoryPath, metadataPath: sourceMetadataPath }) => {
127
+ if (typeof sourceDirectoryPath !== 'string' || sourceDirectoryPath.trim() === '') {
128
+ throw new Error('Путь к директории файлов не должен быть пустым');
129
+ }
130
+
131
+ if (typeof sourceMetadataPath !== 'string' || sourceMetadataPath.trim() === '') {
132
+ throw new Error('Путь к файлу метаданных не должен быть пустым');
133
+ }
134
+
135
+ const directoryPath = resolve(sourceDirectoryPath);
136
+ const metadataPath = resolve(sourceMetadataPath);
137
+ let operationQueue = Promise.resolve();
138
+
139
+ await Promise.all([mkdir(directoryPath, { recursive: true }), mkdir(dirname(metadataPath), { recursive: true })]);
140
+ await readMetadata(metadataPath);
141
+
142
+ const schedule = (operation) => {
143
+ const pendingOperation = operationQueue.then(operation);
144
+
145
+ operationQueue = pendingOperation.catch(() => undefined);
146
+
147
+ return pendingOperation;
148
+ };
149
+
150
+ const get = (id) =>
151
+ schedule(async () => {
152
+ const files = await readMetadata(metadataPath);
153
+ const file = files.find((item) => item.id === id);
154
+
155
+ if (file == null) {
156
+ throw createHttpError(404, 'Файл не найден');
157
+ }
158
+
159
+ const path = join(directoryPath, file.id);
160
+
161
+ try {
162
+ await access(path);
163
+ } catch (error) {
164
+ if (error?.code === 'ENOENT') {
165
+ throw createHttpError(404, 'Файл не найден');
166
+ }
167
+
168
+ throw error;
169
+ }
170
+
171
+ return { file, path };
172
+ });
173
+
174
+ const upload = ({ maxFileSize, mimeType, name, stream }) =>
175
+ schedule(async () => {
176
+ const files = await readMetadata(metadataPath);
177
+ const id = createFileId(files);
178
+ const path = join(directoryPath, id);
179
+ const temporaryPath = `${path}.upload`;
180
+ let size = 0;
181
+
182
+ try {
183
+ await pipeline(
184
+ stream,
185
+ createSizeLimiter(maxFileSize, (value) => (size = value)),
186
+ createWriteStream(temporaryPath, { flags: 'wx' }),
187
+ );
188
+ await rename(temporaryPath, path);
189
+
190
+ const file = { id, mimeType, name, size, url: `/_files/${id}` };
191
+
192
+ files.push(file);
193
+ await writeMetadata(metadataPath, files);
194
+
195
+ return file;
196
+ } catch (error) {
197
+ await Promise.all([rm(temporaryPath, { force: true }), rm(path, { force: true })]);
198
+ throw error;
199
+ }
200
+ });
201
+
202
+ const remove = (id) =>
203
+ schedule(async () => {
204
+ const files = await readMetadata(metadataPath);
205
+ const index = files.findIndex((file) => file.id === id);
206
+
207
+ if (index === -1) {
208
+ throw createHttpError(404, 'Файл не найден');
209
+ }
210
+
211
+ const [file] = files.splice(index, 1);
212
+ const path = join(directoryPath, file.id);
213
+ const temporaryPath = `${path}.${randomBytes(6).toString('hex')}.delete`;
214
+
215
+ try {
216
+ await rename(path, temporaryPath);
217
+ } catch (error) {
218
+ if (error?.code === 'ENOENT') {
219
+ throw createHttpError(404, 'Файл не найден');
220
+ }
221
+
222
+ throw error;
223
+ }
224
+
225
+ try {
226
+ await writeMetadata(metadataPath, files);
227
+ } catch (error) {
228
+ await rename(temporaryPath, path);
229
+ throw error;
230
+ }
231
+
232
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
233
+
234
+ return file;
235
+ });
236
+
237
+ return { get, remove, upload };
238
+ };
239
+
240
+ const getDownloadName = (name) => encodeURIComponent(basename(name)).replaceAll("'", '%27');
241
+
242
+ export const registerFileRoutes = async (server, { directoryPath, maxFileSize, metadataPath }) => {
243
+ const store = await createFileStore({ directoryPath, metadataPath });
244
+
245
+ server.register((fileServer, _options, done) => {
246
+ fileServer.removeAllContentTypeParsers();
247
+ fileServer.addContentTypeParser('*', (_request, payload, parserDone) => parserDone(null, payload));
248
+
249
+ fileServer.post('/_files', async (request, reply) => {
250
+ const contentLength = Number(request.headers['content-length']);
251
+
252
+ if (Number.isFinite(contentLength) && contentLength > maxFileSize) {
253
+ throw createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`);
254
+ }
255
+
256
+ const file = await store.upload({
257
+ maxFileSize,
258
+ mimeType: getMimeType(request.headers['content-type']),
259
+ name: getContentName(request.headers['content-name']),
260
+ stream: request.body,
261
+ });
262
+
263
+ return reply.code(201).send(file);
264
+ });
265
+
266
+ fileServer.get('/_files/:id', async (request, reply) => {
267
+ const { file, path } = await store.get(request.params.id);
268
+
269
+ reply.header('Content-Disposition', `inline; filename*=UTF-8''${getDownloadName(file.name)}`);
270
+ reply.type(file.mimeType);
271
+
272
+ return reply.send(createReadStream(path));
273
+ });
274
+
275
+ fileServer.delete('/_files/:id', async (request) => store.remove(request.params.id));
276
+ done();
277
+ });
278
+ };
@@ -101,6 +101,7 @@ const addReverseRelations = (schemas, rawSchemas, resources, componentNames) =>
101
101
  };
102
102
 
103
103
  const createParameters = () => ({
104
+ ContentName: { description: 'URI-encoded relative file name', in: 'header', name: 'Content-Name', required: true, schema: { type: 'string' } },
104
105
  Embed: { description: 'Relationship paths to embed', explode: true, in: 'query', name: '_embed', schema: { items: { type: 'string' }, type: 'array' }, style: 'form' },
105
106
  Id: { in: 'path', name: 'id', required: true, schema: { type: 'string' } },
106
107
  Page: { in: 'query', name: '_page', required: false, schema: { default: 1, minimum: 1, type: 'integer' } },
@@ -114,6 +115,40 @@ const createResponse = (description, schema) => ({ description, ...(schema != nu
114
115
  const createParameterReference = (name) => ({ $ref: `#/components/parameters/${name}` });
115
116
  const createRequestBody = (name) => ({ required: true, ...createJsonContent(createSchemaReference(name)) });
116
117
 
118
+ const createFilePaths = () => ({
119
+ '/_files': {
120
+ post: {
121
+ operationId: 'uploadFile',
122
+ parameters: [createParameterReference('ContentName')],
123
+ requestBody: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, required: true },
124
+ responses: {
125
+ 201: createResponse('Uploaded', createSchemaReference('UploadedFile')),
126
+ 400: createResponse('Invalid request', createSchemaReference('Error')),
127
+ 413: createResponse('File is too large', createSchemaReference('Error')),
128
+ 415: createResponse('Unsupported media type', createSchemaReference('Error')),
129
+ },
130
+ tags: ['files'],
131
+ },
132
+ },
133
+ '/_files/{id}': {
134
+ delete: {
135
+ operationId: 'deleteFileById',
136
+ parameters: [createParameterReference('Id')],
137
+ responses: { 200: createResponse('Deleted', createSchemaReference('UploadedFile')), 404: createResponse('Not found', createSchemaReference('Error')) },
138
+ tags: ['files'],
139
+ },
140
+ get: {
141
+ operationId: 'getFileById',
142
+ parameters: [createParameterReference('Id')],
143
+ responses: {
144
+ 200: { content: { 'application/octet-stream': { schema: { format: 'binary', type: 'string' } } }, description: 'File contents' },
145
+ 404: createResponse('Not found', createSchemaReference('Error')),
146
+ },
147
+ tags: ['files'],
148
+ },
149
+ },
150
+ });
151
+
117
152
  const createResourcePaths = (resource, componentName) => {
118
153
  const resourceName = toPascalCase(resource);
119
154
 
@@ -181,8 +216,8 @@ const getOperationIds = (resource) => {
181
216
  return [`get${resourceName}`, `post${resourceName}`, `delete${resourceName}ById`, `get${resourceName}ById`, `patch${resourceName}ById`, `put${resourceName}ById`];
182
217
  };
183
218
 
184
- const validateGeneratedNames = (resources, componentNames) => {
185
- const schemaOwners = new Map([['Error', 'встроенная схема ошибки']]);
219
+ const validateGeneratedNames = (resources, componentNames, files) => {
220
+ const schemaOwners = new Map([['Error', 'встроенная схема ошибки'], ...(files ? [['UploadedFile', 'встроенная схема файла']] : [])]);
186
221
  const operationOwners = new Map();
187
222
 
188
223
  resources.forEach((resource) => {
@@ -218,10 +253,10 @@ const validateGeneratedNames = (resources, componentNames) => {
218
253
  * Creates an OpenAPI 3.0 document from a database and optional schema configuration.
219
254
  * @param {Record<string, Array<Record<string, unknown>>>} database Database contents.
220
255
  * @param {Record<string, unknown>} [schemaConfig] Schema configuration.
221
- * @param {{ host?: string, port?: number }} [serverOptions] Server address used in the generated document.
256
+ * @param {{ files?: boolean, host?: string, port?: number }} [serverOptions] Server address and optional features used in the generated document.
222
257
  * @returns {Record<string, unknown>} OpenAPI document.
223
258
  */
224
- export function createOpenApiDocument(database, schemaConfig = {}, { host = DEFAULT_HOST, port = DEFAULT_PORT } = {}) {
259
+ export function createOpenApiDocument(database, schemaConfig = {}, { files = false, host = DEFAULT_HOST, port = DEFAULT_PORT } = {}) {
225
260
  validateDatabase(database);
226
261
 
227
262
  if (!isObject(schemaConfig)) {
@@ -239,7 +274,7 @@ export function createOpenApiDocument(database, schemaConfig = {}, { host = DEFA
239
274
  }),
240
275
  );
241
276
 
242
- validateGeneratedNames(resources, componentNames);
277
+ validateGeneratedNames(resources, componentNames, files);
243
278
 
244
279
  const rawSchemas = Object.fromEntries(
245
280
  resources.map((resource) => {
@@ -250,7 +285,16 @@ export function createOpenApiDocument(database, schemaConfig = {}, { host = DEFA
250
285
  return [resource, applyConfiguredFields(ensureGeneratedIdSchema(configuredSchema), resource, resourceConfig)];
251
286
  }),
252
287
  );
253
- const schemas = { Error: { properties: { error: { type: 'string' } }, required: ['error'], type: 'object' } };
288
+ const schemas = {
289
+ Error: { properties: { error: { type: 'string' } }, required: ['error'], type: 'object' },
290
+ ...(files && {
291
+ UploadedFile: {
292
+ properties: { id: { type: 'string' }, mimeType: { type: 'string' }, name: { type: 'string' }, size: { minimum: 0, type: 'integer' }, url: { type: 'string' } },
293
+ required: ['id', 'mimeType', 'name', 'size', 'url'],
294
+ type: 'object',
295
+ },
296
+ }),
297
+ };
254
298
 
255
299
  resources.forEach((resource) => {
256
300
  const componentName = componentNames[resource];
@@ -280,8 +324,8 @@ export function createOpenApiDocument(database, schemaConfig = {}, { host = DEFA
280
324
  components: { parameters: createParameters(), schemas },
281
325
  info: isObject(schemaConfig.$info) ? schemaConfig.$info : { title: 'Deep JSON Server API', version: '1.0.0' },
282
326
  openapi: '3.0.3',
283
- paths: Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource]))),
327
+ paths: Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource])), files ? createFilePaths() : {}),
284
328
  servers: [{ url: getServerUrl(host, port) }],
285
- tags: resources.map((resource) => ({ name: resource })),
329
+ tags: [...resources.map((resource) => ({ name: resource })), ...(files ? [{ name: 'files' }] : [])],
286
330
  };
287
331
  }
@@ -9,13 +9,13 @@ export { createOpenApiDocument } from './document.js';
9
9
 
10
10
  /**
11
11
  * Generates an OpenAPI YAML file.
12
- * @param {{ databasePath: string, host?: string, outputPath: string, port?: number, schemaPath: string }} options Generation options.
12
+ * @param {{ databasePath: string, files?: boolean, host?: string, outputPath: string, port?: number, schemaPath?: string }} options Generation options.
13
13
  * @returns {Promise<Record<string, unknown>>} Generated OpenAPI document.
14
14
  */
15
- export async function generateOpenApi({ databasePath, host, outputPath, port, schemaPath }) {
15
+ export async function generateOpenApi({ databasePath, files = false, host, outputPath, port, schemaPath }) {
16
16
  const database = await readDatabaseFile(databasePath);
17
17
  const schemaConfig = await readSchemaConfig(schemaPath);
18
- const document = createOpenApiDocument(database, schemaConfig, { host, port });
18
+ const document = createOpenApiDocument(database, schemaConfig, { files, host, port });
19
19
  const resolvedOutputPath = resolve(outputPath);
20
20
 
21
21
  await mkdir(dirname(resolvedOutputPath), { recursive: true });
package/src/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Fastify from 'fastify';
2
- import { DEFAULT_HOST, DEFAULT_PORT, MAX_PAGE_SIZE } from './constants.js';
2
+ import { DEFAULT_HOST, DEFAULT_MAX_FILE_SIZE, DEFAULT_PORT, MAX_PAGE_SIZE } from './constants.js';
3
3
  import { createDatabaseStore, createId, findItem, getCollection } from './database.js';
4
+ import { registerFileRoutes } from './files.js';
4
5
  import { readSchemaConfig } from './openapi/config.js';
5
6
  import { createOpenApiDocument } from './openapi/index.js';
6
7
  import { matchesWhere, paginateItems, parsePagination, parseWhere, sortItems, validateWhere } from './query/index.js';
@@ -8,7 +9,7 @@ import { embedItem, parseEmbedPaths, validateEmbedPaths } from './relations.js';
8
9
  import { createHttpError, getResourceNames, isObject, resolveDatabasePath } from './utils.js';
9
10
 
10
11
  const CORS_HEADERS = {
11
- 'Access-Control-Allow-Headers': 'Content-Type',
12
+ 'Access-Control-Allow-Headers': 'Content-Name, Content-Type',
12
13
  'Access-Control-Allow-Methods': 'DELETE, GET, OPTIONS, PATCH, POST, PUT',
13
14
  'Access-Control-Allow-Origin': '*',
14
15
  };
@@ -142,16 +143,20 @@ const registerResourceRoutes = (server, store, resource, document, maxPageSize)
142
143
 
143
144
  /**
144
145
  * Creates a Fastify server without opening a network port.
145
- * @param {{ databasePath: string, logger?: boolean | Record<string, unknown>, maxPageSize?: number, schemaPath?: string }} options Server options.
146
+ * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, schemaPath?: string }} options Server options.
146
147
  * @returns {Promise<import('fastify').FastifyInstance>} Fastify server.
147
148
  */
148
149
  export async function createServer(options) {
149
- const { databasePath, logger = true, maxPageSize = MAX_PAGE_SIZE, schemaPath } = options ?? {};
150
+ const { databasePath, filesDirectoryPath, filesMetadataPath, logger = true, maxFileSize = DEFAULT_MAX_FILE_SIZE, maxPageSize = MAX_PAGE_SIZE, schemaPath } = options ?? {};
150
151
 
151
152
  if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
152
153
  throw new Error('Максимальный размер страницы должен быть положительным целым числом');
153
154
  }
154
155
 
156
+ if (!Number.isInteger(maxFileSize) || maxFileSize < 1) {
157
+ throw new Error('Максимальный размер файла должен быть положительным целым числом');
158
+ }
159
+
155
160
  const store = await createDatabaseStore(databasePath);
156
161
  const schemaConfig = await readSchemaConfig(schemaPath);
157
162
  const document = createOpenApiDocument(store.database.data, schemaConfig);
@@ -180,6 +185,14 @@ export async function createServer(options) {
180
185
  registerResourceRoutes(server, store, resource, document, maxPageSize);
181
186
  });
182
187
 
188
+ if ((filesDirectoryPath == null) !== (filesMetadataPath == null)) {
189
+ throw new Error('Для файловых маршрутов укажите filesDirectoryPath и filesMetadataPath');
190
+ }
191
+
192
+ if (filesDirectoryPath != null) {
193
+ await registerFileRoutes(server, { directoryPath: filesDirectoryPath, maxFileSize, metadataPath: filesMetadataPath });
194
+ }
195
+
183
196
  server.setErrorHandler((error, request, reply) => {
184
197
  const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
185
198
 
@@ -195,18 +208,28 @@ export async function createServer(options) {
195
208
 
196
209
  /**
197
210
  * Creates and starts a Fastify server.
198
- * @param {{ databasePath: string, host?: string, logger?: boolean | Record<string, unknown>, maxPageSize?: number, port?: number, schemaPath?: string }} options Server options.
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.
199
212
  * @returns {Promise<import('fastify').FastifyInstance>} Listening Fastify server.
200
213
  */
201
214
  export async function startServer(options) {
202
- const { databasePath, host = DEFAULT_HOST, logger = true, maxPageSize = MAX_PAGE_SIZE, port = DEFAULT_PORT, schemaPath } = 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 ?? {};
203
226
 
204
227
  if (!Number.isInteger(port) || port < 0 || port > 65_535) {
205
228
  throw new Error('Порт должен быть целым числом от 0 до 65535');
206
229
  }
207
230
 
208
231
  const resolvedDatabasePath = resolveDatabasePath(databasePath);
209
- const server = await createServer({ databasePath: resolvedDatabasePath, logger, maxPageSize, schemaPath });
232
+ const server = await createServer({ databasePath: resolvedDatabasePath, filesDirectoryPath, filesMetadataPath, logger, maxFileSize, maxPageSize, schemaPath });
210
233
 
211
234
  await server.listen({ host, port });
212
235
  server.log.info({ database: resolvedDatabasePath }, 'Deep JSON Server запущен');
@@ -1,4 +1,5 @@
1
1
  export declare const DEFAULT_HOST = "127.0.0.1";
2
+ export declare const DEFAULT_MAX_FILE_SIZE: number;
2
3
  export declare const DEFAULT_PAGE_SIZE = 10;
3
4
  export declare const DEFAULT_PORT = 4001;
4
5
  export declare const MAX_PAGE_SIZE = 1000;
@@ -0,0 +1,5 @@
1
+ export declare const registerFileRoutes: (server: any, { directoryPath, maxFileSize, metadataPath }: {
2
+ directoryPath: any;
3
+ maxFileSize: any;
4
+ metadataPath: any;
5
+ }) => Promise<void>;
@@ -2,10 +2,11 @@
2
2
  * Creates an OpenAPI 3.0 document from a database and optional schema configuration.
3
3
  * @param {Record<string, Array<Record<string, unknown>>>} database Database contents.
4
4
  * @param {Record<string, unknown>} [schemaConfig] Schema configuration.
5
- * @param {{ host?: string, port?: number }} [serverOptions] Server address used in the generated document.
5
+ * @param {{ files?: boolean, host?: string, port?: number }} [serverOptions] Server address and optional features used in the generated document.
6
6
  * @returns {Record<string, unknown>} OpenAPI document.
7
7
  */
8
- export declare function createOpenApiDocument(database: Record<string, Array<Record<string, unknown>>>, schemaConfig?: Record<string, unknown>, { host, port }?: {
8
+ export declare function createOpenApiDocument(database: Record<string, Array<Record<string, unknown>>>, schemaConfig?: Record<string, unknown>, { files, host, port }?: {
9
+ files?: boolean;
9
10
  host?: string;
10
11
  port?: number;
11
12
  }): Record<string, unknown>;
@@ -1,13 +1,14 @@
1
1
  export { createOpenApiDocument } from './document.js';
2
2
  /**
3
3
  * Generates an OpenAPI YAML file.
4
- * @param {{ databasePath: string, host?: string, outputPath: string, port?: number, schemaPath: string }} options Generation options.
4
+ * @param {{ databasePath: string, files?: boolean, host?: string, outputPath: string, port?: number, schemaPath?: string }} options Generation options.
5
5
  * @returns {Promise<Record<string, unknown>>} Generated OpenAPI document.
6
6
  */
7
- export declare function generateOpenApi({ databasePath, host, outputPath, port, schemaPath }: {
7
+ export declare function generateOpenApi({ databasePath, files, host, outputPath, port, schemaPath }: {
8
8
  databasePath: string;
9
+ files?: boolean;
9
10
  host?: string;
10
11
  outputPath: string;
11
12
  port?: number;
12
- schemaPath: string;
13
+ schemaPath?: string;
13
14
  }): Promise<Record<string, unknown>>;
@@ -1,23 +1,29 @@
1
1
  /**
2
2
  * Creates a Fastify server without opening a network port.
3
- * @param {{ databasePath: string, logger?: boolean | Record<string, unknown>, maxPageSize?: number, schemaPath?: string }} options Server options.
3
+ * @param {{ databasePath: string, filesDirectoryPath?: string, filesMetadataPath?: string, logger?: boolean | Record<string, unknown>, maxFileSize?: number, maxPageSize?: number, schemaPath?: string }} options Server options.
4
4
  * @returns {Promise<import('fastify').FastifyInstance>} Fastify server.
5
5
  */
6
6
  export declare function createServer(options: {
7
7
  databasePath: string;
8
+ filesDirectoryPath?: string;
9
+ filesMetadataPath?: string;
8
10
  logger?: boolean | Record<string, unknown>;
11
+ maxFileSize?: number;
9
12
  maxPageSize?: number;
10
13
  schemaPath?: string;
11
14
  }): Promise<import('fastify').FastifyInstance>;
12
15
  /**
13
16
  * Creates and starts a Fastify server.
14
- * @param {{ databasePath: string, host?: string, logger?: boolean | Record<string, unknown>, maxPageSize?: number, port?: number, schemaPath?: string }} options Server options.
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.
15
18
  * @returns {Promise<import('fastify').FastifyInstance>} Listening Fastify server.
16
19
  */
17
20
  export declare function startServer(options: {
18
21
  databasePath: string;
22
+ filesDirectoryPath?: string;
23
+ filesMetadataPath?: string;
19
24
  host?: string;
20
25
  logger?: boolean | Record<string, unknown>;
26
+ maxFileSize?: number;
21
27
  maxPageSize?: number;
22
28
  port?: number;
23
29
  schemaPath?: string;