@ecopex/ecopex-framework 1.0.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.
Files changed (81) hide show
  1. package/.env +73 -0
  2. package/README.md +248 -0
  3. package/bun.lockb +0 -0
  4. package/config/swagger/admin.js +44 -0
  5. package/config/swagger/api.js +19 -0
  6. package/database/migrations/20240000135243_timezones.js +22 -0
  7. package/database/migrations/20240000135244_countries.js +23 -0
  8. package/database/migrations/20240000135244_create_admins_table.js +66 -0
  9. package/database/migrations/20240000135244_currencies.js +21 -0
  10. package/database/migrations/20240000135244_languages.js +21 -0
  11. package/database/migrations/20240000135244_taxes.js +10 -0
  12. package/database/migrations/20240000135245_sites.js +37 -0
  13. package/database/migrations/20240000135246_payment_methods.js +33 -0
  14. package/database/migrations/20251016113547_devices.js +37 -0
  15. package/database/migrations/20251019192600_users.js +62 -0
  16. package/database/migrations/20251019213551_language_lines.js +35 -0
  17. package/database/migrations/20251222214131_category_groups.js +18 -0
  18. package/database/migrations/20251222214619_categories.js +27 -0
  19. package/database/migrations/20251222214848_brands.js +23 -0
  20. package/database/migrations/20251222214946_products.js +30 -0
  21. package/database/migrations/20251222215428_product_images.js +18 -0
  22. package/database/migrations/20251222215553_options.js +30 -0
  23. package/database/migrations/20251222215806_variants.js +23 -0
  24. package/database/migrations/20251222215940_attributes.js +25 -0
  25. package/database/migrations/20251222220135_discounts.js +15 -0
  26. package/database/migrations/20251222220253_reviews.js +22 -0
  27. package/database/migrations/20251222220341_favorites.js +10 -0
  28. package/database/migrations/20251222220422_search_logs.js +17 -0
  29. package/database/migrations/20251222220636_orders.js +16 -0
  30. package/database/migrations/20251222220806_order_items.js +19 -0
  31. package/database/migrations/20251222221317_order_statuses.js +10 -0
  32. package/database/migrations/20251222221446_order_payments.js +13 -0
  33. package/database/migrations/20251222221654_order_addresses.js +23 -0
  34. package/database/migrations/20251222221807_order_status_logs.js +13 -0
  35. package/database/seeds/admins.js +37 -0
  36. package/database/seeds/countries.js +203 -0
  37. package/database/seeds/currencies.js +165 -0
  38. package/database/seeds/languages.js +113 -0
  39. package/database/seeds/timezones.js +149 -0
  40. package/ecosystem.config.js +26 -0
  41. package/env.example +73 -0
  42. package/knexfile.js +3 -0
  43. package/libraries/2fa.js +22 -0
  44. package/libraries/aws.js +63 -0
  45. package/libraries/bcrypt.js +284 -0
  46. package/libraries/controls.js +113 -0
  47. package/libraries/date.js +14 -0
  48. package/libraries/general.js +8 -0
  49. package/libraries/image.js +57 -0
  50. package/libraries/jwt.js +178 -0
  51. package/libraries/knex.js +7 -0
  52. package/libraries/slug.js +14 -0
  53. package/libraries/stores.js +22 -0
  54. package/libraries/upload.js +194 -0
  55. package/locales/en/messages.json +4 -0
  56. package/locales/en/sql.json +3 -0
  57. package/locales/en/validation.json +52 -0
  58. package/locales/es/validation.json +52 -0
  59. package/locales/tr/validation.json +59 -0
  60. package/package.json +75 -0
  61. package/routes/admin/auto/admins.json +63 -0
  62. package/routes/admin/auto/devices.json +37 -0
  63. package/routes/admin/auto/migrations.json +21 -0
  64. package/routes/admin/auto/users.json +61 -0
  65. package/routes/admin/middlewares/index.js +87 -0
  66. package/routes/admin/spec/auth.js +626 -0
  67. package/routes/admin/spec/users.js +3 -0
  68. package/routes/auto/handler.js +635 -0
  69. package/routes/common/auto/countries.json +28 -0
  70. package/routes/common/auto/currencies.json +26 -0
  71. package/routes/common/auto/languages.json +26 -0
  72. package/routes/common/auto/taxes.json +46 -0
  73. package/routes/common/auto/timezones.json +29 -0
  74. package/stores/base.js +73 -0
  75. package/stores/index.js +195 -0
  76. package/utils/i18n.js +187 -0
  77. package/utils/jsonRouteLoader.js +587 -0
  78. package/utils/middleware.js +154 -0
  79. package/utils/routeLoader.js +227 -0
  80. package/workers/admin.js +124 -0
  81. package/workers/api.js +106 -0
@@ -0,0 +1,587 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const handlers = require('@root/routes/auto/handler');
4
+ const general = require('@root/libraries/general');
5
+ const { add_model } = require('@root/stores');
6
+
7
+ /**
8
+ * JSON Route Loader
9
+ * Loads routes from JSON configuration files in routes/admin folder
10
+ */
11
+ class JsonRouteLoader {
12
+ constructor() {
13
+ this.supportedRoutes = ['list', 'get', 'create', 'update', 'delete', 'upload'];
14
+ this.registeredRoutes = new Set();
15
+ this.deleteKeys = ['creatable', 'updatable', 'search_type', 'listable', 'crypted', 'create_required', 'generatable', 'unique', 'example'];
16
+ }
17
+
18
+ /**
19
+ * Load all JSON route configurations from routes/admin folder
20
+ * @param {Object} fastify - Fastify instance
21
+ */
22
+ async loadJsonRoutes(fastify, routesDir, workerName) {
23
+ const adminPath = path.resolve(routesDir);
24
+
25
+ if(!workerName) {
26
+ console.warn(workerName + ' Worker name is required');
27
+ return;
28
+ }
29
+
30
+ if (!fs.existsSync(adminPath)) {
31
+ console.warn(workerName + ' routes directory does not exist auto directory');
32
+ return;
33
+ }
34
+
35
+ // Find all JSON files in routes/admin
36
+ const jsonFiles = fs.readdirSync(adminPath, { withFileTypes: true })
37
+ .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
38
+ .map(dirent => dirent.name);
39
+
40
+ for (const jsonFile of jsonFiles) {
41
+ try {
42
+ await this.loadJsonRoute(fastify, path.join(adminPath, jsonFile), workerName);
43
+ } catch (error) {
44
+ console.log(error.message);
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Load a single JSON route configuration
51
+ * @param {Object} fastify - Fastify instance
52
+ * @param {string} jsonPath - Path to JSON file
53
+ */
54
+ async loadJsonRoute(fastify, jsonPath, workerName) {
55
+ try {
56
+
57
+ const config = require(jsonPath);
58
+ if(!config) {
59
+ console.warn(`Config not found for ${jsonPath}`);
60
+ return;
61
+ }
62
+
63
+ const fileName = jsonPath.split('/').pop().split('.')[0];
64
+ const tableName = config.table;
65
+ const primaryKey = config.primary_key || 'id';
66
+ const store = config.store || null;
67
+ const routes = config.routes || {};
68
+ const schema = config.schema || {};
69
+
70
+ // Validate configuration
71
+ this.validateConfig(config);
72
+
73
+ // Register routes
74
+ for (const route of routes) {
75
+ const routeName = route.action;
76
+ const routeConfig = route;
77
+ if (this.supportedRoutes.includes(routeName)) {
78
+ await this.registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema);
79
+ if(store) {
80
+ await add_model(store, primaryKey, schema);
81
+ }
82
+ } else {
83
+ fastify.log.warn(`Unsupported route type: ${routeName} for table: ${tableName}`);
84
+ }
85
+ }
86
+
87
+ } catch (error) {
88
+ console.log(error);
89
+ return
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Validate JSON configuration
95
+ * @param {Object} config - Configuration object
96
+ */
97
+ validateConfig(config) {
98
+ if (!config.table) {
99
+ throw new Error('Table name is required');
100
+ }
101
+
102
+ if (!config.routes || typeof config.routes !== 'object') {
103
+ throw new Error('Routes object is required');
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Register a route for a specific table and route type
109
+ * @param {Object} fastify - Fastify instance
110
+ * @param {string} tableName - Table name
111
+ * @param {string} routeName - Route name (list, get, create, update, delete)
112
+ * @param {Object} routeConfig - Route configuration
113
+ */
114
+ async registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema) {
115
+
116
+ const routeDefinition = this.generateRouteDefinition(tableName, routeName, routeConfig, primaryKey, fileName, schema);
117
+ const routeKey = `${routeDefinition.method}:${routeDefinition.url}`;
118
+
119
+ if (this.registeredRoutes.has(routeKey)) {
120
+ console.log(`Route already registered, skipping: ${routeKey}`);
121
+ return;
122
+ }
123
+
124
+ this.registeredRoutes.add(routeKey);
125
+
126
+ try {
127
+
128
+ const handler = this.getHandler(routeName);
129
+
130
+ if(routeConfig.security) {
131
+ routeDefinition.schema.security = [{ bearer: [] }];
132
+ }
133
+
134
+ const filters = Object.entries(schema || {}).reduce((acc, [key, value]) => {
135
+ if(value.search_type) {
136
+ acc[key] = value;
137
+ }
138
+ return acc;
139
+ }, {});
140
+
141
+ // Register the route
142
+ fastify.route({
143
+ ...routeDefinition,
144
+ preHandler: async (request, reply, done) => {
145
+ if(routeConfig.security) {
146
+ await require(`@root/routes/${workerName}/middlewares`)[routeConfig.security](request, reply);
147
+ }
148
+ },
149
+ handler: async (request, reply) => {
150
+ // Pass table name and route config to handler as additional parameters
151
+ return handler(request, reply, { tableName, routeConfig, schema, primaryKey, store, filters });
152
+ }
153
+ });
154
+
155
+ } catch (error) {
156
+ console.error(`Error in registerRoute for ${tableName}/${routeName}:`, error.message);
157
+ console.error(`Error details:`, error);
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Generate route definition
164
+ * @param {string} tableName - Table name
165
+ * @param {string} routeName - Route name
166
+ * @param {Object} routeConfig - Route configuration
167
+ * @returns {Object} Route definition
168
+ */
169
+ generateRouteDefinition(tableName, routeName, routeConfig, primaryKey, fileName, schema) {
170
+ const baseUrl = `/${fileName}`;
171
+ primaryKey = primaryKey || 'id';
172
+
173
+ const routeDefinitions = {
174
+ list: {
175
+ method: 'GET',
176
+ url: baseUrl,
177
+ schema: this.generateListSchema(tableName, routeConfig, primaryKey, schema)
178
+ },
179
+ get: {
180
+ method: 'GET',
181
+ url: `${baseUrl}/:${primaryKey}`,
182
+ schema: this.generateGetSchema(tableName, routeConfig, primaryKey, schema)
183
+ },
184
+ create: {
185
+ method: 'POST',
186
+ url: baseUrl,
187
+ schema: this.generateCreateSchema(tableName, routeConfig, primaryKey, schema)
188
+ },
189
+ update: {
190
+ method: 'PUT',
191
+ url: `${baseUrl}/:${primaryKey}`,
192
+ schema: this.generateUpdateSchema(tableName, routeConfig, primaryKey, schema)
193
+ },
194
+ delete: {
195
+ method: 'DELETE',
196
+ url: `${baseUrl}/:${primaryKey}`,
197
+ schema: this.generateDeleteSchema(tableName, routeConfig, primaryKey, schema)
198
+ },
199
+ upload: {
200
+ method: 'POST',
201
+ url: `${baseUrl}/:${primaryKey}/upload`,
202
+ schema: this.generateUploadSchema(tableName, routeConfig, primaryKey, schema),
203
+ validatorCompiler: ({ schema }) => {
204
+ return (data) => {
205
+ return true;
206
+ };
207
+ }
208
+ }
209
+ };
210
+
211
+ return routeDefinitions[routeName];
212
+ }
213
+
214
+ /**
215
+ * Generate list schema
216
+ * @param {string} tableName - Table name
217
+ * @param {Object} routeConfig - Route configuration
218
+ * @param {Object} schema - Schema object
219
+ * @param {string} primaryKey - Primary key
220
+ * @param {Object} filters - Filters object
221
+ * @returns {Object} Schema
222
+ */
223
+ generateListSchema(tableName, routeConfig, primaryKey, schema) {
224
+ const itemSchema = this.generateItemSchema(schema, primaryKey);
225
+
226
+ return {
227
+ description: `Get all ${tableName} with pagination and search`,
228
+ tags: [general.titleCase(tableName)],
229
+ summary: routeConfig.summary || `List of ${tableName} with pagination and search`,
230
+ querystring: {
231
+ type: 'object',
232
+ properties: {
233
+ page: { type: 'integer', minimum: 1, default: 1 },
234
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
235
+ search: { type: 'string' },
236
+ order: { type: 'string', default: `${primaryKey}:desc` },
237
+ ...Object.entries(schema).reduce((acc, [key, value]) => {
238
+ if(value.search_type) {
239
+ acc[key] = { type: value.type, default: value.default || undefined, enum: value.enum || undefined };
240
+ }
241
+ return acc;
242
+ }, {})
243
+ }
244
+ },
245
+ headers: {
246
+ type: 'object',
247
+ properties: {
248
+ locale: { type: 'string', default: 'en' }
249
+ }
250
+ },
251
+ response: {
252
+ 200: {
253
+ type: 'object',
254
+ properties: {
255
+ status: { type: 'boolean', default: true },
256
+ data: {
257
+ type: 'array',
258
+ items: itemSchema,
259
+ additionalProperties: false
260
+ },
261
+ pagination: {
262
+ type: 'object',
263
+ properties: {
264
+ page: { type: 'integer', example: 1 },
265
+ limit: { type: 'integer', example: 10 },
266
+ total: { type: 'integer', example: 100 },
267
+ totalPages: { type: 'integer', example: 10 }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Generate get schema
278
+ * @param {string} tableName - Table name
279
+ * @param {Object} routeConfig - Route configuration
280
+ * @returns {Object} Schema
281
+ */
282
+ generateGetSchema(tableName, routeConfig, primaryKey, schema) {
283
+ const itemSchema = this.generateItemSchema(schema, primaryKey);
284
+ primaryKey = primaryKey || 'id';
285
+
286
+ return {
287
+ description: `Get ${tableName} by ${primaryKey}`,
288
+ tags: [general.titleCase(tableName)],
289
+ summary: routeConfig.summary || `Get ${tableName} by ${primaryKey}`,
290
+ params: {
291
+ type: 'object',
292
+ properties: {
293
+ [primaryKey]: { type: 'integer', minimum: 1 }
294
+ },
295
+ required: [primaryKey],
296
+ additionalProperties: false
297
+ },
298
+ response: {
299
+ 200: {
300
+ type: 'object',
301
+ properties: {
302
+ status: { type: 'boolean', default: true },
303
+ data: itemSchema
304
+ }
305
+ },
306
+ 404: {
307
+ type: 'object',
308
+ properties: {
309
+ status: { type: 'boolean', default: false },
310
+ message: { type: 'string' }
311
+ }
312
+ }
313
+ }
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Generate create schema
319
+ * @param {string} tableName - Table name
320
+ * @param {Object} routeConfig - Route configuration
321
+ * @returns {Object} Schema
322
+ */
323
+ generateCreateSchema(tableName, routeConfig, primaryKey, schema) {
324
+ const bodySchema = this.generateBodySchema(schema, false, primaryKey);
325
+
326
+ return {
327
+ description: `Create new ${tableName}`,
328
+ tags: [general.titleCase(tableName)],
329
+ summary: routeConfig.summary || `Create new ${tableName}`,
330
+ body: bodySchema,
331
+ response: {
332
+ 200: {
333
+ type: 'object',
334
+ properties: {
335
+ status: { type: 'boolean', default: true },
336
+ data: this.generateItemSchema(schema, primaryKey)
337
+ },
338
+ additionalProperties: false
339
+ },
340
+ 400: {
341
+ type: 'object',
342
+ properties: {
343
+ status: { type: 'boolean', default: false },
344
+ message: { type: 'string' }
345
+ }
346
+ }
347
+ }
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Generate update schema
353
+ * @param {string} tableName - Table name
354
+ * @param {Object} routeConfig - Route configuration
355
+ * @returns {Object} Schema
356
+ */
357
+ generateUpdateSchema(tableName, routeConfig, primaryKey, schema) {
358
+ const bodySchema = this.generateBodySchema(schema, true, primaryKey, 'updatable');
359
+
360
+ return {
361
+ description: `Update ${tableName} by ID`,
362
+ tags: [general.titleCase(tableName)],
363
+ summary: routeConfig.summary || `Update ${tableName} by ${primaryKey}`,
364
+ params: {
365
+ type: 'object',
366
+ properties: {
367
+ [primaryKey]: { type: 'integer', minimum: 1 }
368
+ },
369
+ required: [primaryKey],
370
+ additionalProperties: false
371
+ },
372
+ body: bodySchema,
373
+ response: {
374
+ 200: {
375
+ type: 'object',
376
+ properties: {
377
+ status: { type: 'boolean', default: true },
378
+ data: this.generateItemSchema(schema, primaryKey)
379
+ },
380
+ additionalProperties: false
381
+ },
382
+ 404: {
383
+ type: 'object',
384
+ properties: {
385
+ status: { type: 'boolean', default: false },
386
+ message: { type: 'string' }
387
+ }
388
+ }
389
+ }
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Generate delete schema
395
+ * @param {string} tableName - Table name
396
+ * @param {Object} routeConfig - Route configuration
397
+ * @returns {Object} Schema
398
+ */
399
+ generateDeleteSchema(tableName, routeConfig, primaryKey, schema) {
400
+ return {
401
+ description: `Delete ${tableName} by ID`,
402
+ tags: [general.titleCase(tableName)],
403
+ summary: routeConfig.summary || `Delete ${tableName} by ${primaryKey}`,
404
+ params: {
405
+ type: 'object',
406
+ properties: {
407
+ [primaryKey]: { type: 'integer', minimum: 1 }
408
+ },
409
+ required: [primaryKey],
410
+ additionalProperties: false
411
+ },
412
+ response: {
413
+ 200: {
414
+ type: 'object',
415
+ properties: {
416
+ status: { type: 'boolean', default: true },
417
+ data: this.generateItemSchema(schema, primaryKey)
418
+ },
419
+ additionalProperties: false
420
+ },
421
+ 404: {
422
+ type: 'object',
423
+ properties: {
424
+ status: { type: 'boolean', default: false },
425
+ message: { type: 'string' }
426
+ }
427
+ }
428
+ }
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Generate upload schema
434
+ * @param {string} tableName - Table name
435
+ * @param {Object} routeConfig - Route configuration
436
+ * @returns {Object} Schema
437
+ */
438
+ generateUploadSchema(tableName, routeConfig, primaryKey, schema) {
439
+ const uploadField = routeConfig.upload_field || 'file';
440
+ return {
441
+ description: `Upload file for ${tableName} by ${primaryKey}`,
442
+ tags: [general.titleCase(tableName)],
443
+ summary: routeConfig.summary || `Upload file for ${tableName} by ${primaryKey}`,
444
+ params: {
445
+ type: 'object',
446
+ properties: {
447
+ [primaryKey]: { type: 'integer', minimum: 1 }
448
+ },
449
+ required: [primaryKey],
450
+ additionalProperties: false
451
+ },
452
+ consumes: ['multipart/form-data'],
453
+ body: {
454
+ type: 'object',
455
+ properties: {
456
+ [uploadField]: { type: 'string', format: 'binary' }
457
+ },
458
+ additionalProperties: false
459
+ },
460
+ response: {
461
+ 200: {
462
+ type: 'object',
463
+ properties: {
464
+ status: { type: 'boolean', default: true },
465
+ data: this.generateItemSchema(schema, primaryKey)
466
+ },
467
+ additionalProperties: false
468
+ },
469
+ 404: {
470
+ type: 'object',
471
+ properties: {
472
+ status: { type: 'boolean', default: false },
473
+ message: { type: 'string' }
474
+ }
475
+ }
476
+ }
477
+ };
478
+ }
479
+
480
+ /**
481
+ * Generate item schema for responses
482
+ * @param {Object} itemSchema - Item schema from config
483
+ * @returns {Object} Item schema
484
+ */
485
+ generateItemSchema(schema, primaryKey) {
486
+ if (!schema) {
487
+ return { type: 'object' };
488
+ }
489
+
490
+ const withoutReferences = { ...schema };
491
+
492
+ if(Object.entries(withoutReferences).length > 0) {
493
+ for(const [key, value] of Object.entries(withoutReferences)) {
494
+ const withoutReferencesValue = { ...value };
495
+ if(withoutReferencesValue.listable !== false) {
496
+ if(Object.entries(withoutReferencesValue).length > 0) {
497
+ for(const [key2,] of Object.entries(withoutReferencesValue)) {
498
+ if(this.deleteKeys.includes(key2)) {
499
+ delete withoutReferencesValue[key2];
500
+ }
501
+ }
502
+ }
503
+ withoutReferences[key] = value;
504
+ } else {
505
+ delete withoutReferences[key];
506
+ }
507
+ }
508
+ }
509
+
510
+ return {
511
+ type: 'object',
512
+ properties: withoutReferences,
513
+ additionalProperties: false
514
+ };
515
+ }
516
+
517
+ /**
518
+ * Generate body schema for create/update
519
+ * @param {Object} itemSchema - Item schema from config
520
+ * @param {boolean} isUpdate - Whether this is for update (optional fields)
521
+ * @returns {Object} Body schema
522
+ */
523
+ generateBodySchema(schema, isUpdate = false, primaryKey = 'id', type = 'creatable') {
524
+ if (!schema) {
525
+ return { type: 'object' };
526
+ }
527
+
528
+ // Filter out auto-generated fields for create/update
529
+ const filteredSchema = { ...schema };
530
+ delete filteredSchema[primaryKey];
531
+ delete filteredSchema.created_at;
532
+ delete filteredSchema.updated_at;
533
+
534
+ const requiredKeys = Object.keys(filteredSchema).filter(key => type == 'creatable' ? filteredSchema[key].create_required : filteredSchema[key].updatable_required);
535
+ // delete required keys in objects
536
+ const props = Object.entries(filteredSchema).reduce((acc, [key, value]) => {
537
+ if (typeof value === 'object') {
538
+ const withoutReferences = Object.assign({}, value);
539
+ if(withoutReferences[type]) {
540
+ if(Object.entries(withoutReferences).length > 0) {
541
+ for(const [key2,] of Object.entries(withoutReferences)) {
542
+ if(this.deleteKeys.includes(key2)) {
543
+ delete withoutReferences[key2];
544
+ }
545
+ }
546
+ }
547
+
548
+ if(type == 'updatable') {
549
+ delete withoutReferences.default
550
+ }
551
+
552
+ acc[key] = withoutReferences;
553
+ }
554
+ }
555
+ return acc;
556
+ }, {});
557
+
558
+ return {
559
+ type: 'object',
560
+ required: requiredKeys,
561
+ properties: props,
562
+ additionalProperties: false,
563
+ ...(isUpdate ? { minProperties: 1 } : {})
564
+ };
565
+ }
566
+
567
+ /**
568
+ * Get handler function for route type
569
+ * @param {string} routeName - Route name
570
+ * @returns {Function} Handler function
571
+ */
572
+ getHandler(routeName) {
573
+
574
+ const handlerMap = {
575
+ 'list': handlers.getAll,
576
+ 'get': handlers.getById,
577
+ 'create': handlers.create,
578
+ 'update': handlers.update,
579
+ 'delete': handlers.delete,
580
+ 'upload': handlers.upload
581
+ };
582
+
583
+ return handlerMap[routeName];
584
+ }
585
+ }
586
+
587
+ module.exports = JsonRouteLoader;