json-rest-api 1.0.0 → 1.0.2

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 (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
@@ -0,0 +1,366 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * Express Plugin for Hooked API
5
+ *
6
+ * This plugin creates HTTP endpoints for your REST API by translating
7
+ * Express HTTP requests into hooked-api method calls.
8
+ *
9
+ * Features:
10
+ * - Automatic route creation for all scopes (including dynamically added ones)
11
+ * - JSON:API compliant request/response handling
12
+ * - Query parameter parsing (include, fields, filter, sort, page)
13
+ * - Error mapping to HTTP status codes
14
+ * - Content type validation
15
+ * - Middleware injection points
16
+ *
17
+ * Usage:
18
+ * ```javascript
19
+ * import { ExpressPlugin } from './express-plugin.js';
20
+ *
21
+ * const api = new Api({ name: 'my-api', version: '1.0.0' });
22
+ * api.use(RestApiPlugin);
23
+ * api.use(ExpressPlugin, {
24
+ * basePath: '/api', // Default: '/api'
25
+ * strictContentType: true, // Default: true
26
+ * requestSizeLimit: '10mb' // Default: '1mb'
27
+ * });
28
+ *
29
+ * // In your Express app:
30
+ * app.use(api.express.router);
31
+ * // or
32
+ * api.express.mount(app, '/v1');
33
+ * ```
34
+ */
35
+
36
+ export const ExpressPlugin = {
37
+ name: 'express',
38
+ dependencies: ['rest-api'],
39
+
40
+ install({ on, vars, helpers, pluginOptions, log, scopes, api }) {
41
+ const expressOptions = pluginOptions.express || {};
42
+ const basePath = expressOptions.basePath || '/api';
43
+ const strictContentType = expressOptions.strictContentType !== false;
44
+ const requestSizeLimit = expressOptions.requestSizeLimit || '1mb';
45
+
46
+ // Track which scopes have routes created
47
+ const routesCreated = new Set();
48
+
49
+ // Create Express router
50
+ const router = expressOptions.router || express.Router();
51
+
52
+ // Add body parsing middleware
53
+ router.use(express.json({
54
+ limit: requestSizeLimit,
55
+ type: ['application/json', 'application/vnd.api+json']
56
+ }));
57
+
58
+ // Content type validation middleware
59
+ if (strictContentType) {
60
+ router.use((req, res, next) => {
61
+ // Only validate for requests with bodies
62
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
63
+ const contentType = req.get('Content-Type');
64
+
65
+ if (contentType && !contentType.includes('application/vnd.api+json') && !contentType.includes('application/json')) {
66
+ return res.status(415).json({
67
+ errors: [{
68
+ status: '415',
69
+ title: 'Unsupported Media Type',
70
+ detail: `Content-Type must be application/vnd.api+json or application/json`
71
+ }]
72
+ });
73
+ }
74
+ }
75
+ next();
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Query parameter parser
81
+ * Handles both standard notation (filter.status) and bracket notation (filter[status])
82
+ */
83
+ const parseQueryParams = (req) => {
84
+ const query = req.query;
85
+ const queryParams = {};
86
+
87
+ // Parse include (comma-separated string to array)
88
+ if (query.include) {
89
+ queryParams.include = query.include.split(',').map(s => s.trim());
90
+ }
91
+
92
+ // Parse fields (nested object)
93
+ queryParams.fields = {};
94
+
95
+ // Parse filter (object)
96
+ queryParams.filter = {};
97
+
98
+ // Parse page (object)
99
+ queryParams.page = {};
100
+
101
+ // Process all query parameters
102
+ for (const key in query) {
103
+ const value = query[key];
104
+
105
+ // Handle bracket notation: fields[articles]=title,body
106
+ let match;
107
+ if ((match = key.match(/^fields\[(.+)\]$/))) {
108
+ queryParams.fields[match[1]] = value;
109
+ } else if ((match = key.match(/^filter\[(.+)\]$/))) {
110
+ queryParams.filter[match[1]] = value;
111
+ } else if ((match = key.match(/^page\[(.+)\]$/))) {
112
+ queryParams.page[match[1]] = value;
113
+ }
114
+ }
115
+
116
+ // Handle object notation if express query parser is configured
117
+ if (typeof query.fields === 'object' && !Array.isArray(query.fields)) {
118
+ Object.assign(queryParams.fields, query.fields);
119
+ }
120
+ if (typeof query.filter === 'object' && !Array.isArray(query.filter)) {
121
+ Object.assign(queryParams.filter, query.filter);
122
+ }
123
+ if (typeof query.page === 'object' && !Array.isArray(query.page)) {
124
+ Object.assign(queryParams.page, query.page);
125
+ }
126
+
127
+ // Parse sort (comma-separated string to array)
128
+ if (query.sort) {
129
+ queryParams.sort = query.sort.split(',').map(s => s.trim());
130
+ }
131
+
132
+ // Clean up empty objects
133
+ if (Object.keys(queryParams.fields).length === 0) delete queryParams.fields;
134
+ if (Object.keys(queryParams.filter).length === 0) delete queryParams.filter;
135
+ if (Object.keys(queryParams.page).length === 0) delete queryParams.page;
136
+
137
+ return queryParams;
138
+ };
139
+
140
+ /**
141
+ * Error handler - maps API errors to HTTP responses
142
+ */
143
+ const handleError = (error, res) => {
144
+ let status = 500;
145
+ let errorResponse = {
146
+ errors: [{
147
+ status: '500',
148
+ title: 'Internal Server Error',
149
+ detail: error.message
150
+ }]
151
+ };
152
+
153
+ // Check for RestApiError types by code
154
+ if (error.code) {
155
+ switch (error.code) {
156
+ case 'REST_API_VALIDATION':
157
+ status = 422;
158
+ errorResponse.errors = [{
159
+ status: '422',
160
+ title: 'Validation Error',
161
+ detail: error.message,
162
+ source: error.details
163
+ }];
164
+ if (error.violations) {
165
+ errorResponse.errors = error.violations.map(v => ({
166
+ status: '422',
167
+ title: 'Validation Error',
168
+ detail: v.message,
169
+ source: { pointer: v.field }
170
+ }));
171
+ }
172
+ break;
173
+
174
+ case 'REST_API_RESOURCE':
175
+ // Map subtype to status
176
+ switch (error.subtype) {
177
+ case 'not_found':
178
+ status = 404;
179
+ errorResponse.errors[0].status = '404';
180
+ errorResponse.errors[0].title = 'Not Found';
181
+ break;
182
+ case 'conflict':
183
+ status = 409;
184
+ errorResponse.errors[0].status = '409';
185
+ errorResponse.errors[0].title = 'Conflict';
186
+ break;
187
+ case 'forbidden':
188
+ status = 403;
189
+ errorResponse.errors[0].status = '403';
190
+ errorResponse.errors[0].title = 'Forbidden';
191
+ break;
192
+ default:
193
+ status = 400;
194
+ errorResponse.errors[0].status = '400';
195
+ errorResponse.errors[0].title = 'Bad Request';
196
+ }
197
+ break;
198
+
199
+ case 'REST_API_PAYLOAD':
200
+ status = 400;
201
+ errorResponse.errors = [{
202
+ status: '400',
203
+ title: 'Bad Request',
204
+ detail: error.message,
205
+ source: { pointer: error.path }
206
+ }];
207
+ break;
208
+ }
209
+ }
210
+
211
+ res.status(status).json(errorResponse);
212
+ };
213
+
214
+ /**
215
+ * Creates request handler for a specific HTTP method
216
+ */
217
+ const createRequestHandler = (scopeName, methodName) => {
218
+ return async (req, res) => {
219
+ try {
220
+ const params = {};
221
+
222
+ // Add ID parameter for single-resource operations
223
+ if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
224
+ params.id = req.params.id;
225
+ }
226
+
227
+ // Add query parameters
228
+ if (['query', 'get'].includes(methodName)) {
229
+ params.queryParams = parseQueryParams(req);
230
+ }
231
+
232
+ // Add body for mutations
233
+ if (['post', 'put', 'patch'].includes(methodName)) {
234
+ params.inputRecord = req.body;
235
+
236
+ // For PUT and PATCH, also parse query params
237
+ if (['put', 'patch'].includes(methodName)) {
238
+ params.queryParams = parseQueryParams(req);
239
+ }
240
+ }
241
+
242
+ // Store Express request/response in params for plugins that need it
243
+ params._expressReq = req;
244
+ params._expressRes = res;
245
+
246
+ log.debug(`HTTP ${req.method} ${req.path}`, { scopeName, methodName, params });
247
+
248
+ // Call the API method
249
+ const scope = scopes[scopeName];
250
+ if (!scope) {
251
+ return handleError({
252
+ code: 'REST_API_RESOURCE',
253
+ subtype: 'not_found',
254
+ message: `Scope '${scopeName}' not found`
255
+ }, res);
256
+ }
257
+
258
+ const result = await scope[methodName](params);
259
+
260
+ // Send response
261
+ res.set('Content-Type', 'application/vnd.api+json');
262
+
263
+ switch (methodName) {
264
+ case 'post':
265
+ res.status(201).json(result);
266
+ break;
267
+ case 'delete':
268
+ res.sendStatus(204);
269
+ break;
270
+ default:
271
+ res.json(result);
272
+ }
273
+ } catch (error) {
274
+ handleError(error, res);
275
+ }
276
+ };
277
+ };
278
+
279
+ /**
280
+ * Creates routes for a scope
281
+ */
282
+ const createRoutesForScope = (scopeName) => {
283
+ if (routesCreated.has(scopeName)) {
284
+ log.trace(`Routes already created for scope '${scopeName}'`);
285
+ return;
286
+ }
287
+
288
+ const scopePath = `${basePath}/${scopeName}`;
289
+
290
+ log.info(`Creating Express routes for scope '${scopeName}' at ${scopePath}`);
291
+
292
+ // Apply any before middleware
293
+ const beforeMiddleware = expressOptions.middleware?.beforeScope?.[scopeName] || [];
294
+
295
+ // GET /api/{scope} - Query collection
296
+ router.get(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'query'));
297
+
298
+ // GET /api/{scope}/{id} - Get single resource
299
+ router.get(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'get'));
300
+
301
+ // POST /api/{scope} - Create resource
302
+ router.post(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'post'));
303
+
304
+ // PUT /api/{scope}/{id} - Replace resource
305
+ router.put(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'put'));
306
+
307
+ // PATCH /api/{scope}/{id} - Update resource
308
+ router.patch(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'patch'));
309
+
310
+ // DELETE /api/{scope}/{id} - Delete resource
311
+ router.delete(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'delete'));
312
+
313
+ // Apply any after middleware
314
+ const afterMiddleware = expressOptions.middleware?.afterScope?.[scopeName] || [];
315
+ if (afterMiddleware.length > 0) {
316
+ router.use(scopePath, ...afterMiddleware);
317
+ }
318
+
319
+ routesCreated.add(scopeName);
320
+ };
321
+
322
+ // Listen for scope additions
323
+ on('scope:added', 'createExpressRoutes', ({ eventData }) => {
324
+ createRoutesForScope(eventData.scopeName);
325
+ });
326
+
327
+ // Create routes for any existing scopes
328
+ for (const scopeName of Object.keys(scopes)) {
329
+ createRoutesForScope(scopeName);
330
+ }
331
+
332
+ // 404 handler for unmatched routes within the API
333
+ router.use((req, res) => {
334
+ res.status(404).json({
335
+ errors: [{
336
+ status: '404',
337
+ title: 'Not Found',
338
+ detail: `The requested endpoint ${req.method} ${req.path} does not exist`
339
+ }]
340
+ });
341
+ });
342
+
343
+ // Apply global middleware if configured
344
+ let finalRouter = router;
345
+ if (expressOptions.middleware?.beforeAll) {
346
+ const globalRouter = express.Router();
347
+ globalRouter.use(...expressOptions.middleware.beforeAll);
348
+ globalRouter.use(router);
349
+ finalRouter = globalRouter;
350
+ }
351
+
352
+ // Initialize express namespace
353
+ api.express = {};
354
+
355
+ // Store router in express namespace
356
+ api.express.router = finalRouter;
357
+
358
+ // Add convenient mounting method
359
+ api.express.mount = (app, path = '') => {
360
+ app.use(path, finalRouter);
361
+ log.info(`Express routes mounted at ${path || '/'}`);
362
+ };
363
+
364
+ log.info('Express plugin initialized successfully');
365
+ }
366
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * File Upload Example
3
+ *
4
+ * This example demonstrates how to use the file handling plugin
5
+ * with various storage backends and protocols.
6
+ */
7
+
8
+ import { Api } from 'hooked-api';
9
+ import {
10
+ RestApiPlugin,
11
+ FileHandlingPlugin,
12
+ ExpressPlugin,
13
+ HttpPlugin
14
+ } from '../index.js';
15
+ import { LocalStorage } from '../plugins/storage/local-storage.js';
16
+ import { S3Storage } from '../plugins/storage/s3-storage.js';
17
+ import express from 'express';
18
+
19
+ // Create API instance
20
+ const api = new Api({
21
+ name: 'file-upload-example',
22
+ version: '1.0.0'
23
+ });
24
+
25
+ // Configure storage backends
26
+ const localStorage = new LocalStorage({
27
+ directory: './uploads/images',
28
+ baseUrl: 'http://localhost:3000/uploads/images'
29
+ });
30
+
31
+ const s3Storage = new S3Storage({
32
+ bucket: 'my-app-uploads',
33
+ region: 'us-east-1',
34
+ prefix: 'user-uploads/',
35
+ mockMode: true // Using mock mode for this example
36
+ });
37
+
38
+ // Use plugins - order matters!
39
+ api.use(RestApiPlugin);
40
+ api.use(FileHandlingPlugin); // Must come after RestApiPlugin
41
+ api.use(ExpressPlugin, {
42
+ fileParser: 'busboy', // or 'formidable'
43
+ fileParserOptions: {
44
+ limits: {
45
+ fileSize: 10 * 1024 * 1024, // 10MB
46
+ files: 5 // Max 5 files per request
47
+ }
48
+ }
49
+ });
50
+
51
+ // Define schemas with file fields
52
+ api.addScope('users', {
53
+ schema: {
54
+ name: { type: 'string', required: true },
55
+ email: { type: 'string', required: true },
56
+ avatar: {
57
+ type: 'file',
58
+ storage: localStorage,
59
+ accepts: ['image/jpeg', 'image/png', 'image/gif'],
60
+ maxSize: '5mb',
61
+ required: false
62
+ }
63
+ }
64
+ });
65
+
66
+ api.addScope('documents', {
67
+ schema: {
68
+ title: { type: 'string', required: true },
69
+ description: { type: 'string' },
70
+ file: {
71
+ type: 'file',
72
+ storage: s3Storage,
73
+ accepts: ['application/pdf', 'application/msword', 'text/plain'],
74
+ maxSize: '50mb',
75
+ required: true
76
+ },
77
+ thumbnail: {
78
+ type: 'file',
79
+ storage: localStorage,
80
+ accepts: ['image/*'],
81
+ maxSize: '2mb',
82
+ required: false
83
+ }
84
+ }
85
+ });
86
+
87
+ // Add storage helpers for demonstration
88
+ api.helpers.dataPost = async ({ scopeName, inputRecord }) => {
89
+ console.log(`Creating ${scopeName}:`, inputRecord);
90
+
91
+ // Simulate database save
92
+ const id = Math.random().toString(36).substring(7);
93
+
94
+ return {
95
+ data: {
96
+ type: scopeName,
97
+ id: id,
98
+ attributes: inputRecord.data.attributes
99
+ }
100
+ };
101
+ };
102
+
103
+ api.helpers.dataGet = async ({ scopeName, id }) => {
104
+ // Simulate database fetch
105
+ return {
106
+ data: {
107
+ type: scopeName,
108
+ id: id,
109
+ attributes: {
110
+ // Would normally fetch from database
111
+ }
112
+ }
113
+ };
114
+ };
115
+
116
+ // Create Express app
117
+ const app = express();
118
+
119
+ // Serve uploaded files statically
120
+ app.use('/uploads', express.static('./uploads'));
121
+
122
+ // Mount API
123
+ api.express.mount(app, '/api');
124
+
125
+ // Add a simple upload form for testing
126
+ app.get('/', (req, res) => {
127
+ res.send(`
128
+ <!DOCTYPE html>
129
+ <html>
130
+ <head>
131
+ <title>File Upload Example</title>
132
+ </head>
133
+ <body>
134
+ <h1>File Upload Example</h1>
135
+
136
+ <h2>Create User with Avatar</h2>
137
+ <form action="/api/users" method="POST" enctype="multipart/form-data">
138
+ <div>
139
+ <label>Name: <input name="name" required></label>
140
+ </div>
141
+ <div>
142
+ <label>Email: <input name="email" type="email" required></label>
143
+ </div>
144
+ <div>
145
+ <label>Avatar: <input name="avatar" type="file" accept="image/*"></label>
146
+ </div>
147
+ <button type="submit">Create User</button>
148
+ </form>
149
+
150
+ <h2>Upload Document</h2>
151
+ <form action="/api/documents" method="POST" enctype="multipart/form-data">
152
+ <div>
153
+ <label>Title: <input name="title" required></label>
154
+ </div>
155
+ <div>
156
+ <label>Description: <textarea name="description"></textarea></label>
157
+ </div>
158
+ <div>
159
+ <label>Document: <input name="file" type="file" accept=".pdf,.doc,.txt" required></label>
160
+ </div>
161
+ <div>
162
+ <label>Thumbnail: <input name="thumbnail" type="file" accept="image/*"></label>
163
+ </div>
164
+ <button type="submit">Upload Document</button>
165
+ </form>
166
+
167
+ <hr>
168
+ <p>Note: The form data will be automatically converted to JSON:API format by the file handling plugin.</p>
169
+ </body>
170
+ </html>
171
+ `);
172
+ });
173
+
174
+ // Start server
175
+ const PORT = process.env.PORT || 3000;
176
+ app.listen(PORT, () => {
177
+ console.log(`File upload example running at http://localhost:${PORT}`);
178
+ console.log(`API endpoints:`);
179
+ console.log(` POST http://localhost:${PORT}/api/users (with multipart form data)`);
180
+ console.log(` POST http://localhost:${PORT}/api/documents (with multipart form data)`);
181
+ });
182
+
183
+ // Example cURL commands:
184
+ /*
185
+ # Upload user with avatar
186
+ curl -X POST http://localhost:3000/api/users \
187
+ -F "name=John Doe" \
188
+ -F "email=john@example.com" \
189
+ -F "avatar=@/path/to/avatar.jpg"
190
+
191
+ # Upload document with thumbnail
192
+ curl -X POST http://localhost:3000/api/documents \
193
+ -F "title=Important Document" \
194
+ -F "description=This is a very important document" \
195
+ -F "file=@/path/to/document.pdf" \
196
+ -F "thumbnail=@/path/to/thumbnail.png"
197
+
198
+ # The file handling plugin will:
199
+ # 1. Detect the multipart upload
200
+ # 2. Parse the files
201
+ # 3. Validate file types and sizes
202
+ # 4. Upload to configured storage
203
+ # 5. Replace file fields with URLs
204
+ # 6. Convert to JSON:API format
205
+ */
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Filename Strategy Examples
3
+ *
4
+ * Different approaches to handling uploaded filenames
5
+ */
6
+
7
+ import { LocalStorage } from '../plugins/storage/local-storage.js';
8
+
9
+ // Strategy 1: Random hash (most secure, default)
10
+ const hashStorage = new LocalStorage({
11
+ directory: './uploads/images',
12
+ baseUrl: '/uploads/images',
13
+ nameStrategy: 'hash',
14
+ preserveExtension: true
15
+ });
16
+ // Uploads as: "a7f8d9e2b4c6e1f3.jpg"
17
+
18
+ // Strategy 2: Timestamp-based (sortable)
19
+ const timestampStorage = new LocalStorage({
20
+ directory: './uploads/documents',
21
+ baseUrl: '/uploads/documents',
22
+ nameStrategy: 'timestamp',
23
+ preserveExtension: true
24
+ });
25
+ // Uploads as: "1672531200000_a8f9.pdf"
26
+
27
+ // Strategy 3: Sanitized original (user-friendly)
28
+ const originalStorage = new LocalStorage({
29
+ directory: './uploads/user-files',
30
+ baseUrl: '/uploads/user-files',
31
+ nameStrategy: 'original',
32
+ preserveExtension: true,
33
+ maxFilenameLength: 100
34
+ });
35
+ // "My Resume 2024!.pdf" uploads as: "My_Resume_2024_.pdf"
36
+ // Duplicates become: "My_Resume_2024_1.pdf", "My_Resume_2024_2.pdf"
37
+
38
+ // Strategy 4: Custom naming function
39
+ const customStorage = new LocalStorage({
40
+ directory: './uploads/profiles',
41
+ baseUrl: '/uploads/profiles',
42
+ nameStrategy: 'custom',
43
+ nameGenerator: async (file) => {
44
+ // Example: user ID + timestamp
45
+ const userId = file.userId || 'anonymous';
46
+ const timestamp = Date.now();
47
+ return `user_${userId}_${timestamp}`;
48
+ }
49
+ });
50
+ // Uploads as: "user_12345_1672531200000.jpg"
51
+
52
+ // Strategy 5: High security (no extensions)
53
+ const secureStorage = new LocalStorage({
54
+ directory: './uploads/secure',
55
+ baseUrl: '/uploads/secure',
56
+ nameStrategy: 'hash',
57
+ preserveExtension: false // Store as .bin
58
+ });
59
+ // All files upload as: "a7f8d9e2b4c6e1f3.bin"
60
+
61
+ // Strategy 6: Whitelist extensions
62
+ const whitelistStorage = new LocalStorage({
63
+ directory: './uploads/images',
64
+ baseUrl: '/uploads/images',
65
+ nameStrategy: 'hash',
66
+ preserveExtension: true,
67
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp']
68
+ });
69
+ // Only allows specific image extensions
70
+
71
+ // Example: Organization by date
72
+ const dateOrganizedStorage = new LocalStorage({
73
+ directory: './uploads',
74
+ baseUrl: '/uploads',
75
+ nameStrategy: 'custom',
76
+ nameGenerator: async (file) => {
77
+ const date = new Date();
78
+ const year = date.getFullYear();
79
+ const month = String(date.getMonth() + 1).padStart(2, '0');
80
+ const day = String(date.getDate()).padStart(2, '0');
81
+ const random = crypto.randomBytes(8).toString('hex');
82
+
83
+ // Create date-based subdirectory
84
+ const dateDir = `${year}/${month}/${day}`;
85
+ await fs.mkdir(path.join('./uploads', dateDir), { recursive: true });
86
+
87
+ return `${dateDir}/${random}`;
88
+ }
89
+ });
90
+ // Uploads as: "2024/01/15/a7f8d9e2b4c6e1f3.jpg"
91
+
92
+ // Example: Content-based naming
93
+ const contentStorage = new LocalStorage({
94
+ directory: './uploads/media',
95
+ baseUrl: '/uploads/media',
96
+ nameStrategy: 'custom',
97
+ nameGenerator: async (file) => {
98
+ // Generate hash of file content for deduplication
99
+ const crypto = require('crypto');
100
+ const hash = crypto.createHash('sha256');
101
+ hash.update(file.data);
102
+ const contentHash = hash.digest('hex').substring(0, 16);
103
+
104
+ // Prefix by type
105
+ const typePrefix = file.mimetype.startsWith('image/') ? 'img' :
106
+ file.mimetype.startsWith('video/') ? 'vid' :
107
+ file.mimetype.startsWith('audio/') ? 'aud' : 'doc';
108
+
109
+ return `${typePrefix}_${contentHash}`;
110
+ }
111
+ });
112
+ // Uploads as: "img_a7f8d9e2b4c6e1f3.jpg" (same file always gets same name)
113
+
114
+ // Example usage in schema
115
+ api.addScope('products', {
116
+ schema: {
117
+ name: { type: 'string', required: true },
118
+
119
+ // Product images need user-friendly names
120
+ mainImage: {
121
+ type: 'file',
122
+ storage: originalStorage,
123
+ accepts: ['image/*']
124
+ },
125
+
126
+ // Technical documents use timestamp
127
+ manual: {
128
+ type: 'file',
129
+ storage: timestampStorage,
130
+ accepts: ['application/pdf']
131
+ },
132
+
133
+ // User uploads use high security
134
+ attachment: {
135
+ type: 'file',
136
+ storage: secureStorage,
137
+ accepts: ['*']
138
+ }
139
+ }
140
+ });