@ecopex/ecopex-framework 1.0.13 → 1.0.15
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/config/swagger.js +17 -17
- package/index.js +4 -1
- package/knexfile.js +47 -1
- package/libraries/fastify.js +2 -1
- package/package.json +3 -2
- package/routes/auto/handler.js +146 -20
- package/utils/i18n.js +24 -5
- package/utils/jsonRouteLoader.js +37 -18
package/config/swagger.js
CHANGED
|
@@ -17,25 +17,25 @@ module.exports = {
|
|
|
17
17
|
produces: ['application/json'],
|
|
18
18
|
components: {
|
|
19
19
|
securitySchemes: {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
bearer: {
|
|
21
|
+
type: 'http',
|
|
22
|
+
scheme: 'bearer',
|
|
23
|
+
bearerFormat: 'JWT',
|
|
24
|
+
description: 'Enter the token with the `Bearer: ` prefix, e.g. "Bearer abcde12345".'
|
|
25
|
+
},
|
|
26
|
+
apiKey: {
|
|
27
|
+
type: 'apiKey',
|
|
28
|
+
in: 'header',
|
|
29
|
+
name: 'apiKey',
|
|
30
|
+
description: 'Enter token e.g. "abcde1-234533-333312-2222".'
|
|
31
|
+
}
|
|
32
32
|
},
|
|
33
33
|
headers: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
description: 'Headers',
|
|
35
|
+
scheme: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {}
|
|
38
|
+
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
package/index.js
CHANGED
|
@@ -4,11 +4,13 @@ const JWT = require('./libraries/jwt');
|
|
|
4
4
|
const BCRYPT = require('./libraries/bcrypt');
|
|
5
5
|
const date = require('./libraries/date');
|
|
6
6
|
const twofactor = require('./libraries/2fa');
|
|
7
|
+
const i18n = require('./utils/i18n');
|
|
7
8
|
|
|
8
9
|
const jwt = new JWT();
|
|
9
10
|
const bcrypt = new BCRYPT();
|
|
10
11
|
|
|
11
12
|
const start = async (config) => {
|
|
13
|
+
i18n.setConfig(config);
|
|
12
14
|
await knex.initialize(config);
|
|
13
15
|
await fastifyStart(config);
|
|
14
16
|
}
|
|
@@ -19,5 +21,6 @@ module.exports = {
|
|
|
19
21
|
date: date,
|
|
20
22
|
jwt: jwt,
|
|
21
23
|
bcrypt: bcrypt,
|
|
22
|
-
twofactor: twofactor
|
|
24
|
+
twofactor: twofactor,
|
|
25
|
+
i18n: i18n
|
|
23
26
|
}
|
package/knexfile.js
CHANGED
|
@@ -1 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
const config = require('../../../config');
|
|
2
|
+
|
|
3
|
+
const services = Object.entries(config).filter(([, value]) => value.database).map(([key, ]) => key);
|
|
4
|
+
|
|
5
|
+
let readline = require('readline-sync');
|
|
6
|
+
let service = readline.question("For which service do you want to migrate? (" + services.join(', ') + ') default:' + services[0] + ' => ');
|
|
7
|
+
service = service || services[0];
|
|
8
|
+
|
|
9
|
+
if(!services.includes(service)) {
|
|
10
|
+
console.log('Service not found');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
development: {
|
|
16
|
+
client: 'mysql2',
|
|
17
|
+
connection: {
|
|
18
|
+
host: config[service].database.host,
|
|
19
|
+
port: config[service].database.port,
|
|
20
|
+
user: config[service].database.user,
|
|
21
|
+
password: config[service].database.password,
|
|
22
|
+
database: config[service].database.database
|
|
23
|
+
},
|
|
24
|
+
migrations: {
|
|
25
|
+
directory: '../../../database/' + service + '/migrations'
|
|
26
|
+
},
|
|
27
|
+
seeds: {
|
|
28
|
+
directory: '../../../database/' + service + '/seeds'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
production: {
|
|
32
|
+
client: 'mysql2',
|
|
33
|
+
connection: {
|
|
34
|
+
host: config[service].database.host,
|
|
35
|
+
port: config[service].database.port,
|
|
36
|
+
user: config[service].database.user,
|
|
37
|
+
password: config[service].database.password,
|
|
38
|
+
database: config[service].database.database
|
|
39
|
+
},
|
|
40
|
+
migrations: {
|
|
41
|
+
directory: '../../../database/' + service + '/migrations'
|
|
42
|
+
},
|
|
43
|
+
seeds: {
|
|
44
|
+
directory: '../../../database/' + service + '/seeds'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
package/libraries/fastify.js
CHANGED
|
@@ -21,7 +21,8 @@ async function registerPlugins(config) {
|
|
|
21
21
|
// CORS plugin
|
|
22
22
|
await fastifyInstance.register(require('@fastify/cors'), {
|
|
23
23
|
origin: config.cors || '*',
|
|
24
|
-
|
|
24
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
25
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'locale', 'timezone', 'device']
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
// Swagger documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopex/ecopex-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Javascript Framework for API and Admin Panel",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"pm2": "^5.3.0",
|
|
49
49
|
"qrcode": "^1.5.4",
|
|
50
50
|
"slugify": "^1.6.6",
|
|
51
|
-
"uuid": "^9.0.1"
|
|
51
|
+
"uuid": "^9.0.1",
|
|
52
|
+
"readline-sync": "^1.4.10"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"nodemon": "^3.0.1",
|
package/routes/auto/handler.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const knex = require('../../libraries/knex');
|
|
2
2
|
const BCRYPT = require('../../libraries/bcrypt');
|
|
3
|
+
const returnSlug = require('../../libraries/slug');
|
|
3
4
|
const bcrypt = new BCRYPT();
|
|
4
5
|
const i18n = require('../../utils/i18n');
|
|
5
6
|
const { uploadFile } = require('../../libraries/upload');
|
|
6
7
|
const { randomChars } = require('../../libraries/controls');
|
|
7
|
-
const { get_all, get_by_primary_key, create_item, update_item, delete_item } = require('../../stores');
|
|
8
|
+
const { get_all, get_by_primary_key, create_item, update_item, delete_item, stores } = require('../../stores');
|
|
8
9
|
const { config } = require('../../stores/config');
|
|
9
10
|
/**
|
|
10
11
|
* Get all records with pagination, search, and join tables
|
|
@@ -25,8 +26,9 @@ async function getAll(request, reply, routeOptions = {}) {
|
|
|
25
26
|
const tableName = routeOptions.tableName || 'users';
|
|
26
27
|
const routeConfig = routeOptions.routeConfig || {};
|
|
27
28
|
const primaryKey = routeOptions.primaryKey || 'id';
|
|
28
|
-
const filters = routeOptions.filters ||
|
|
29
|
-
const
|
|
29
|
+
const filters = routeOptions.filters || false;
|
|
30
|
+
const owned = routeOptions.owned || false;
|
|
31
|
+
const joinTables = routeOptions.joinTables || false;
|
|
30
32
|
const splitOrder = order.split(':');
|
|
31
33
|
const orderBy = `${tableName}.${(splitOrder[0] || primaryKey)}`;
|
|
32
34
|
const orderDirection = splitOrder[1] || 'desc';
|
|
@@ -46,18 +48,51 @@ async function getAll(request, reply, routeOptions = {}) {
|
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
if(
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if(owned) {
|
|
52
|
+
for(let i = 0; i < Object.entries(owned).length; i++) {
|
|
53
|
+
const owned_field = Object.entries(owned)[i];
|
|
54
|
+
if(owned_field[1].from == 'request') {
|
|
55
|
+
query.where(tableName + '.' + owned_field[0], (owned_field[1].operator || '='), getText(request, owned_field[1].path));
|
|
56
|
+
} else {
|
|
57
|
+
query.where(tableName + '.' + owned_field[0], (owned_field[1].operator || '='), owned_field[1].value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if(routeConfig.conditions) {
|
|
63
|
+
for(let i = 0; i < Object.entries(routeConfig.conditions).length; i++) {
|
|
64
|
+
const condition = Object.entries(routeConfig.conditions)[i];
|
|
65
|
+
if(condition[1].value_type == 'request') {
|
|
66
|
+
query.where(condition[0], (condition[1].operator || '='), getText(request, condition[1].value));
|
|
67
|
+
} else {
|
|
68
|
+
query.where(condition[0], (condition[1].operator || '='), condition[1].value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
if(joinTables) {
|
|
55
74
|
for(let i = 0; i < Object.values(joinTables).length; i++) {
|
|
56
75
|
const tableKey = Object.keys(joinTables)[i];
|
|
57
76
|
let joinTable = Object.values(joinTables)[i];
|
|
58
|
-
if(joinTable.type == 'one-to-one') {
|
|
59
|
-
if(joinTable.
|
|
60
|
-
|
|
77
|
+
if(joinTable.type == 'one-to-one' || (joinTable.type == 'one-to-many' && joinTable.localized)) {
|
|
78
|
+
if(joinTable.localized) {
|
|
79
|
+
const stored_languages = stores.languages || false;
|
|
80
|
+
|
|
81
|
+
if(stored_languages) {
|
|
82
|
+
const find_language = [...stored_languages.data.values()].find(n => n.code === request.locale);
|
|
83
|
+
|
|
84
|
+
if(find_language) {
|
|
85
|
+
if(joinTable.extra_where) {
|
|
86
|
+
joinTable.extra_where += ` AND ${tableKey}.language_id = ${find_language.language_id}`;
|
|
87
|
+
} else {
|
|
88
|
+
joinTable.extra_where = `${tableKey}.language_id = ${find_language.language_id}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if(joinTable.extra_where && joinTable.extra_where != '') {
|
|
95
|
+
query.leftJoin(joinTable.table + ' as ' + tableKey, knex.connection.db.raw(`${tableName}.${joinTable.foreign_key} = ${tableKey}.${joinTable.primary_key} AND ${joinTable.extra_where}`));
|
|
61
96
|
} else {
|
|
62
97
|
query.leftJoin(joinTable.table + ' as ' + tableKey, `${tableName}.${joinTable.foreign_key}`, `${tableKey}.${joinTable.primary_key}`);
|
|
63
98
|
}
|
|
@@ -150,9 +185,21 @@ async function getById(request, reply, routeOptions = {}, inner = false) {
|
|
|
150
185
|
try {
|
|
151
186
|
const tableName = routeOptions.tableName || 'users';
|
|
152
187
|
const joinTables = routeOptions.joinTables || {};
|
|
188
|
+
const owned = routeOptions.owned || false;
|
|
153
189
|
const item_query = knex.connection.db(tableName)
|
|
154
190
|
.select(`${tableName}.*`)
|
|
155
191
|
.where(request.params);
|
|
192
|
+
|
|
193
|
+
if(owned) {
|
|
194
|
+
for(let i = 0; i < Object.entries(owned).length; i++) {
|
|
195
|
+
const owned_field = Object.entries(owned)[i];
|
|
196
|
+
if(owned_field[1].from == 'request') {
|
|
197
|
+
item_query.where(tableName + '.' + owned_field[0], (owned_field[1].operator || '='), getText(request, owned_field[1].path));
|
|
198
|
+
} else {
|
|
199
|
+
item_query.where(tableName + '.' + owned_field[0], (owned_field[1].operator || '='), owned_field[1].value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
156
203
|
|
|
157
204
|
if(joinTables) {
|
|
158
205
|
for(let i = 0; i < Object.values(joinTables).length; i++) {
|
|
@@ -173,8 +220,21 @@ async function getById(request, reply, routeOptions = {}, inner = false) {
|
|
|
173
220
|
|
|
174
221
|
const item_data = await item_query.first();
|
|
175
222
|
|
|
176
|
-
|
|
223
|
+
if(!item_data) {
|
|
224
|
+
if(!inner) {
|
|
225
|
+
return reply.status(404).send({
|
|
226
|
+
status: false,
|
|
227
|
+
message: request.t('messages.record_not_found')
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
return {
|
|
231
|
+
status: false,
|
|
232
|
+
message: request.t('messages.record_not_found')
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
177
236
|
|
|
237
|
+
let item = {}
|
|
178
238
|
|
|
179
239
|
if(joinTables) {
|
|
180
240
|
item = {
|
|
@@ -187,6 +247,14 @@ async function getById(request, reply, routeOptions = {}, inner = false) {
|
|
|
187
247
|
item[join_table[0]] = item_data[join_table[0]];
|
|
188
248
|
}
|
|
189
249
|
}
|
|
250
|
+
const join_one_to_many = Object.entries(joinTables).filter(n => n[1].type === 'one-to-many');
|
|
251
|
+
if(join_one_to_many.length > 0) {
|
|
252
|
+
for(let i = 0; i < join_one_to_many.length; i++) {
|
|
253
|
+
const join_table = join_one_to_many[i];
|
|
254
|
+
const join_table_data = await knex.connection.db(join_table[1].table).where(join_table[1].foreign_key, item_data[tableName][join_table[1].primary_key]);
|
|
255
|
+
item[join_table[0]] = join_table_data;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
190
258
|
} else {
|
|
191
259
|
item = item_data;
|
|
192
260
|
}
|
|
@@ -242,34 +310,61 @@ async function getById(request, reply, routeOptions = {}, inner = false) {
|
|
|
242
310
|
*/
|
|
243
311
|
async function create(request, reply, routeOptions = {}) {
|
|
244
312
|
|
|
313
|
+
const tableName = routeOptions.tableName || 'users';
|
|
314
|
+
const primaryKey = routeOptions.primaryKey || 'id';
|
|
315
|
+
const owned = routeOptions.owned || false;
|
|
316
|
+
const schema = routeOptions.schema || {};
|
|
317
|
+
|
|
318
|
+
if(routeOptions.unique && routeOptions.unique.length > 0) {
|
|
319
|
+
const extra_where = routeOptions.unique.map(n => `${tableName}.${n} = ${request.body[n]}`).join(' AND ');
|
|
320
|
+
const check_unique = await checkUnique(null, null, tableName, extra_where);
|
|
321
|
+
if(check_unique) {
|
|
322
|
+
return reply.status(400).send({
|
|
323
|
+
status: false,
|
|
324
|
+
message: request.t('messages.unique_constraint_violation')
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
245
329
|
if(routeOptions.store) {
|
|
246
330
|
return create_item(routeOptions.store, request.body, reply, routeOptions);
|
|
247
331
|
}
|
|
248
332
|
|
|
249
333
|
try {
|
|
250
|
-
const tableName = routeOptions.tableName || 'users';
|
|
251
|
-
const primaryKey = routeOptions.primaryKey || 'id';
|
|
252
|
-
const schema = routeOptions.schema || {};
|
|
253
334
|
|
|
254
335
|
// Use payload field if available, otherwise use request.body directly
|
|
255
336
|
let data = request.body;
|
|
256
337
|
|
|
257
338
|
if (schema) {
|
|
258
|
-
const payloadFields = Object.entries(schema).filter(n => n[1].creatable || n[1].generatable);
|
|
339
|
+
const payloadFields = Object.entries(schema).filter(n => n[1].creatable || n[1].generatable || n[1].slug);
|
|
259
340
|
data = {};
|
|
260
341
|
|
|
261
342
|
for (const field of payloadFields) {
|
|
262
|
-
if (request.body[field[0]] !== undefined || schema[field[0]].generatable) {
|
|
343
|
+
if (request.body[field[0]] !== undefined || schema[field[0]].generatable || schema[field[0]].slug) {
|
|
263
344
|
if (schema[field[0]].crypted) {
|
|
264
345
|
data[field[0]] = await bcrypt.hash(request.body[field[0]]);
|
|
265
346
|
} else if(schema[field[0]].generatable) {
|
|
266
347
|
data[field[0]] = await generateRandomString(typeof schema[field[0]].generatable === 'string' ? request.body[schema[field[0]].generatable] : null, field[0], tableName);
|
|
348
|
+
} else if(schema[field[0]].slug) {
|
|
349
|
+
const extra_where = schema[field[0]].slug.unique.map(n => `${tableName}.${n} = ${request.body[n]}`).join(' AND ');
|
|
350
|
+
data[field[0]] = await generateSlug(request.body[schema[field[0]].slug.from], field[0], tableName, extra_where, schema[field[0]].slug.random_chars || 5);
|
|
267
351
|
} else {
|
|
268
352
|
data[field[0]] = request.body[field[0]];
|
|
269
353
|
}
|
|
270
354
|
}
|
|
271
355
|
}
|
|
272
356
|
}
|
|
357
|
+
|
|
358
|
+
if(owned) {
|
|
359
|
+
for(let i = 0; i < Object.entries(owned).length; i++) {
|
|
360
|
+
const owned_field = Object.entries(owned)[i];
|
|
361
|
+
if(owned_field[1].from == 'request') {
|
|
362
|
+
data[owned_field[0]] = getText(request, owned_field[1].path);
|
|
363
|
+
} else {
|
|
364
|
+
data[owned_field[0]] = owned_field[1].value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
273
368
|
|
|
274
369
|
try {
|
|
275
370
|
const [id] = await knex.connection.db(tableName)
|
|
@@ -520,6 +615,19 @@ const generateRandomString = async (from_string = null, field_name = null, table
|
|
|
520
615
|
return random_string;
|
|
521
616
|
}
|
|
522
617
|
|
|
618
|
+
const generateSlug = async (from_string = null, field_name = null, tableName = null, extra_where = null, random_chars = 5) => {
|
|
619
|
+
let slug = returnSlug(from_string);
|
|
620
|
+
|
|
621
|
+
const existing_item = await checkUnique(slug, field_name, tableName, extra_where);
|
|
622
|
+
|
|
623
|
+
if(existing_item) {
|
|
624
|
+
slug = returnSlug(from_string + '-' + randomChars(random_chars));
|
|
625
|
+
return generateSlug(slug, field_name, tableName, extra_where, random_chars);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return slug;
|
|
629
|
+
}
|
|
630
|
+
|
|
523
631
|
/**
|
|
524
632
|
* Check if the value is unique
|
|
525
633
|
* @param {string} value - The value to check unique
|
|
@@ -527,9 +635,16 @@ const generateRandomString = async (from_string = null, field_name = null, table
|
|
|
527
635
|
* @param {string} tableName - The table name to check unique
|
|
528
636
|
* @returns {Promise<boolean>} - The unique status
|
|
529
637
|
*/
|
|
530
|
-
const checkUnique = async (value = null, field_name = null, tableName = null) => {
|
|
531
|
-
|
|
532
|
-
|
|
638
|
+
const checkUnique = async (value = null, field_name = null, tableName = null, extra_where = null) => {
|
|
639
|
+
let query = knex.connection.db(tableName).select(field_name);
|
|
640
|
+
if(value && field_name) {
|
|
641
|
+
query.where(field_name, value);
|
|
642
|
+
}
|
|
643
|
+
if(extra_where) {
|
|
644
|
+
query.where(knex.connection.db.raw(extra_where));
|
|
645
|
+
}
|
|
646
|
+
const existing_item = await query;
|
|
647
|
+
return existing_item.length > 0 ? true : false;
|
|
533
648
|
}
|
|
534
649
|
|
|
535
650
|
/**
|
|
@@ -590,7 +705,7 @@ const processJoinTables = async (items, joinTables) => {
|
|
|
590
705
|
return items;
|
|
591
706
|
}
|
|
592
707
|
|
|
593
|
-
const length_of_one_to_many = Object.entries(joinTables).filter(n => n[1].type === 'one-to-many');
|
|
708
|
+
const length_of_one_to_many = Object.entries(joinTables).filter(n => n[1].type === 'one-to-many' && !n[1].localized);
|
|
594
709
|
if(length_of_one_to_many.length > 0) {
|
|
595
710
|
for(let i = 0; i < length_of_one_to_many.length; i++) {
|
|
596
711
|
const joinTable = length_of_one_to_many[i][1];
|
|
@@ -601,7 +716,7 @@ const processJoinTables = async (items, joinTables) => {
|
|
|
601
716
|
const personal_items = knex.connection.db(joinTable.table).whereIn(joinTable.table + '.' + joinTable.foreign_key, personal_ids);
|
|
602
717
|
|
|
603
718
|
if(joinTable.extra_where) {
|
|
604
|
-
personal_items.where(db.raw(joinTable.table + '.' + joinTable.extra_where));
|
|
719
|
+
personal_items.where(knex.connection.db.raw(joinTable.table + '.' + joinTable.extra_where));
|
|
605
720
|
}
|
|
606
721
|
|
|
607
722
|
if(joinTable.join_table) {
|
|
@@ -628,6 +743,17 @@ const processJoinTables = async (items, joinTables) => {
|
|
|
628
743
|
}
|
|
629
744
|
}
|
|
630
745
|
|
|
746
|
+
const getText = (row, path) => {
|
|
747
|
+
const keys = path.split('.')
|
|
748
|
+
let value = row
|
|
749
|
+
|
|
750
|
+
for (const key of keys) {
|
|
751
|
+
value = value[key] || false
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return value
|
|
755
|
+
}
|
|
756
|
+
|
|
631
757
|
module.exports = {
|
|
632
758
|
getAll,
|
|
633
759
|
getById,
|
package/utils/i18n.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { config } = require('../stores/config');
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Internationalization utility for multi-language support
|
|
@@ -8,13 +7,18 @@ const { config } = require('../stores/config');
|
|
|
8
7
|
class I18n {
|
|
9
8
|
constructor() {
|
|
10
9
|
this.locales = {};
|
|
11
|
-
this.defaultLocale =
|
|
12
|
-
this.supportedLocales =
|
|
10
|
+
this.defaultLocale = 'en';
|
|
11
|
+
this.supportedLocales = ['en'];
|
|
13
12
|
this.localesPath = path.join(__dirname, '../locales');
|
|
14
13
|
this.localesBackend = path.join('./locales');
|
|
15
14
|
this.loadLocales();
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
setConfig(config) {
|
|
18
|
+
this.defaultLocale = config.default_locale || 'en';
|
|
19
|
+
this.supportedLocales = config.supported_locales || ['en'];
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
/**
|
|
19
23
|
* Load all locale files
|
|
20
24
|
*/
|
|
@@ -62,15 +66,30 @@ class I18n {
|
|
|
62
66
|
* @param {Object} params - Parameters to replace in the translation
|
|
63
67
|
* @returns {string} Translated string
|
|
64
68
|
*/
|
|
65
|
-
t(key,
|
|
69
|
+
t(key, params = {}, locale = this.defaultLocale) {
|
|
66
70
|
const keys = key.split('.');
|
|
67
|
-
|
|
71
|
+
|
|
72
|
+
if(!this.locales[locale]) {
|
|
73
|
+
this.locales[locale] = {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if(!fs.existsSync(path.join(this.localesBackend, locale))) {
|
|
77
|
+
// create directory
|
|
78
|
+
fs.mkdirSync(path.join(this.localesBackend, locale), { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let translation = this.locales[locale];
|
|
68
82
|
const fileName = keys.length > 0 ? keys[0] : 'index';
|
|
69
83
|
|
|
70
84
|
if(!translation[fileName]) {
|
|
71
85
|
translation[fileName] = {};
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
if(!fs.existsSync(path.join(this.localesBackend, locale, `${fileName}.json`))) {
|
|
89
|
+
this.writeFile(key, locale, params);
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
if(keys.length > 1) {
|
|
75
94
|
for(const k of keys.slice(1)) {
|
|
76
95
|
if(translation[fileName] && translation[fileName][k] !== undefined) {
|
package/utils/jsonRouteLoader.js
CHANGED
|
@@ -13,7 +13,7 @@ class JsonRouteLoader {
|
|
|
13
13
|
constructor() {
|
|
14
14
|
this.supportedRoutes = ['list', 'get', 'create', 'update', 'delete', 'upload'];
|
|
15
15
|
this.registeredRoutes = new Set();
|
|
16
|
-
this.deleteKeys = ['creatable', 'updatable', 'search_type', 'listable', 'crypted', 'create_required', 'generatable', 'unique', 'example'];
|
|
16
|
+
this.deleteKeys = ['creatable', 'updatable', 'search_type', 'listable', 'crypted', 'create_required', 'generatable', 'unique', 'example', 'slug', 'exists'];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -67,6 +67,8 @@ class JsonRouteLoader {
|
|
|
67
67
|
const store = config.store || null;
|
|
68
68
|
const routes = config.routes || {};
|
|
69
69
|
const schema = config.schema || {};
|
|
70
|
+
const unique = config.unique || [];
|
|
71
|
+
const owned = config.owned || {};
|
|
70
72
|
const joinTables = config.join_tables || {};
|
|
71
73
|
|
|
72
74
|
// Validate configuration
|
|
@@ -77,7 +79,7 @@ class JsonRouteLoader {
|
|
|
77
79
|
const routeName = route.action;
|
|
78
80
|
const routeConfig = route;
|
|
79
81
|
if (this.supportedRoutes.includes(routeName)) {
|
|
80
|
-
await this.registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema, joinTables, type);
|
|
82
|
+
await this.registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema, joinTables, type, unique, owned);
|
|
81
83
|
if(store) {
|
|
82
84
|
await add_model(store, primaryKey, schema);
|
|
83
85
|
}
|
|
@@ -113,7 +115,7 @@ class JsonRouteLoader {
|
|
|
113
115
|
* @param {string} routeName - Route name (list, get, create, update, delete)
|
|
114
116
|
* @param {Object} routeConfig - Route configuration
|
|
115
117
|
*/
|
|
116
|
-
async registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema, joinTables, type = 'service') {
|
|
118
|
+
async registerRoute(fastify, tableName, routeName, routeConfig, primaryKey, workerName, fileName, store, schema, joinTables, type = 'service', unique = [], owned = {}) {
|
|
117
119
|
|
|
118
120
|
const routeDefinition = this.generateRouteDefinition(tableName, routeName, routeConfig, primaryKey, fileName, schema, type, joinTables);
|
|
119
121
|
const routeKey = `${routeDefinition.method}:${routeDefinition.url}`;
|
|
@@ -150,7 +152,7 @@ class JsonRouteLoader {
|
|
|
150
152
|
},
|
|
151
153
|
handler: async (request, reply) => {
|
|
152
154
|
// Pass table name and route config to handler as additional parameters
|
|
153
|
-
return handler(request, reply, { tableName, routeConfig, schema, primaryKey, store, filters, joinTables });
|
|
155
|
+
return handler(request, reply, { tableName, routeConfig, schema, primaryKey, store, filters, joinTables, unique, owned });
|
|
154
156
|
}
|
|
155
157
|
});
|
|
156
158
|
|
|
@@ -170,9 +172,17 @@ class JsonRouteLoader {
|
|
|
170
172
|
*/
|
|
171
173
|
generateRouteDefinition(tableName, routeName, routeConfig, primaryKey, fileName, schema, type = 'service', joinTables = {}) {
|
|
172
174
|
const prefix = configStore.prefix ? configStore.prefix + '/' : '';
|
|
173
|
-
|
|
175
|
+
let cleanPath = routeConfig.path || false
|
|
176
|
+
if(cleanPath && cleanPath.startsWith('/')) {
|
|
177
|
+
cleanPath = cleanPath.slice(1);
|
|
178
|
+
}
|
|
179
|
+
let baseUrl = `/${prefix}${(cleanPath ? cleanPath : fileName)}`;
|
|
174
180
|
primaryKey = primaryKey || 'id';
|
|
175
181
|
|
|
182
|
+
if(!routeConfig.path) {
|
|
183
|
+
baseUrl += `/${primaryKey}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
const routeDefinitions = {
|
|
177
187
|
list: {
|
|
178
188
|
method: 'GET',
|
|
@@ -181,7 +191,7 @@ class JsonRouteLoader {
|
|
|
181
191
|
},
|
|
182
192
|
get: {
|
|
183
193
|
method: 'GET',
|
|
184
|
-
url: `${baseUrl}
|
|
194
|
+
url: `${baseUrl}`,
|
|
185
195
|
schema: this.generateGetSchema(tableName, routeConfig, primaryKey, schema, type, joinTables)
|
|
186
196
|
},
|
|
187
197
|
create: {
|
|
@@ -191,17 +201,17 @@ class JsonRouteLoader {
|
|
|
191
201
|
},
|
|
192
202
|
update: {
|
|
193
203
|
method: 'PUT',
|
|
194
|
-
url: `${baseUrl}
|
|
204
|
+
url: `${baseUrl}`,
|
|
195
205
|
schema: this.generateUpdateSchema(tableName, routeConfig, primaryKey, schema, type, joinTables)
|
|
196
206
|
},
|
|
197
207
|
delete: {
|
|
198
208
|
method: 'DELETE',
|
|
199
|
-
url: `${baseUrl}
|
|
209
|
+
url: `${baseUrl}`,
|
|
200
210
|
schema: this.generateDeleteSchema(tableName, routeConfig, primaryKey, schema, type, joinTables)
|
|
201
211
|
},
|
|
202
212
|
upload: {
|
|
203
213
|
method: 'POST',
|
|
204
|
-
url: `${baseUrl}
|
|
214
|
+
url: `${baseUrl}`,
|
|
205
215
|
schema: this.generateUploadSchema(tableName, routeConfig, primaryKey, schema, type, joinTables),
|
|
206
216
|
validatorCompiler: ({ schema }) => {
|
|
207
217
|
return (data) => {
|
|
@@ -224,7 +234,7 @@ class JsonRouteLoader {
|
|
|
224
234
|
* @returns {Object} Schema
|
|
225
235
|
*/
|
|
226
236
|
generateListSchema(tableName, routeConfig, primaryKey, schema, type = 'service', joinTables = {}) {
|
|
227
|
-
const itemSchema = this.generateItemSchema(schema, primaryKey, joinTables);
|
|
237
|
+
const itemSchema = this.generateItemSchema('list', schema, primaryKey, joinTables);
|
|
228
238
|
|
|
229
239
|
return {
|
|
230
240
|
description: `Get all ${tableName} with pagination and search`,
|
|
@@ -239,7 +249,7 @@ class JsonRouteLoader {
|
|
|
239
249
|
order: { type: 'string', default: `${primaryKey}:desc` },
|
|
240
250
|
...Object.entries(schema).reduce((acc, [key, value]) => {
|
|
241
251
|
if(value.search_type) {
|
|
242
|
-
acc[key] = { type: value.type, default: value.default || undefined, enum: value.enum || undefined };
|
|
252
|
+
acc[key] = { type: value.type == 'boolean' ? 'integer' : value.type, default: value.default || undefined, enum: value.enum || undefined };
|
|
243
253
|
}
|
|
244
254
|
return acc;
|
|
245
255
|
}, {})
|
|
@@ -283,7 +293,7 @@ class JsonRouteLoader {
|
|
|
283
293
|
* @returns {Object} Schema
|
|
284
294
|
*/
|
|
285
295
|
generateGetSchema(tableName, routeConfig, primaryKey, schema, type = 'service', joinTables = {}) {
|
|
286
|
-
const itemSchema = this.generateItemSchema(schema, primaryKey, joinTables);
|
|
296
|
+
const itemSchema = this.generateItemSchema('get', schema, primaryKey, joinTables);
|
|
287
297
|
primaryKey = primaryKey || 'id';
|
|
288
298
|
|
|
289
299
|
return {
|
|
@@ -336,7 +346,7 @@ class JsonRouteLoader {
|
|
|
336
346
|
type: 'object',
|
|
337
347
|
properties: {
|
|
338
348
|
status: { type: 'boolean', default: true },
|
|
339
|
-
data: this.generateItemSchema(schema, primaryKey, joinTables)
|
|
349
|
+
data: this.generateItemSchema('create', schema, primaryKey, joinTables)
|
|
340
350
|
},
|
|
341
351
|
additionalProperties: false
|
|
342
352
|
},
|
|
@@ -378,7 +388,7 @@ class JsonRouteLoader {
|
|
|
378
388
|
type: 'object',
|
|
379
389
|
properties: {
|
|
380
390
|
status: { type: 'boolean', default: true },
|
|
381
|
-
data: this.generateItemSchema(schema, primaryKey, joinTables)
|
|
391
|
+
data: this.generateItemSchema('update', schema, primaryKey, joinTables)
|
|
382
392
|
},
|
|
383
393
|
additionalProperties: false
|
|
384
394
|
},
|
|
@@ -417,7 +427,7 @@ class JsonRouteLoader {
|
|
|
417
427
|
type: 'object',
|
|
418
428
|
properties: {
|
|
419
429
|
status: { type: 'boolean', default: true },
|
|
420
|
-
data: this.generateItemSchema(schema, primaryKey, joinTables)
|
|
430
|
+
data: this.generateItemSchema('delete', schema, primaryKey, joinTables)
|
|
421
431
|
},
|
|
422
432
|
additionalProperties: false
|
|
423
433
|
},
|
|
@@ -465,7 +475,7 @@ class JsonRouteLoader {
|
|
|
465
475
|
type: 'object',
|
|
466
476
|
properties: {
|
|
467
477
|
status: { type: 'boolean', default: true },
|
|
468
|
-
data: this.generateItemSchema(schema, primaryKey, joinTables)
|
|
478
|
+
data: this.generateItemSchema('upload', schema, primaryKey, joinTables)
|
|
469
479
|
},
|
|
470
480
|
additionalProperties: false
|
|
471
481
|
},
|
|
@@ -485,7 +495,7 @@ class JsonRouteLoader {
|
|
|
485
495
|
* @param {Object} itemSchema - Item schema from config
|
|
486
496
|
* @returns {Object} Item schema
|
|
487
497
|
*/
|
|
488
|
-
generateItemSchema(schema, primaryKey, joinTables = {}) {
|
|
498
|
+
generateItemSchema(type, schema, primaryKey, joinTables = {}) {
|
|
489
499
|
if (!schema) {
|
|
490
500
|
return { type: 'object' };
|
|
491
501
|
}
|
|
@@ -512,12 +522,21 @@ class JsonRouteLoader {
|
|
|
512
522
|
|
|
513
523
|
if(Object.entries(joinTables).length > 0) {
|
|
514
524
|
for(const [key, value] of Object.entries(joinTables)) {
|
|
515
|
-
if(value.type == 'one-to-one') {
|
|
525
|
+
if(value.type == 'one-to-one' || (value.type == 'one-to-many' && value.localized && type == 'list')) {
|
|
516
526
|
withoutReferences[value.table] = {
|
|
517
527
|
type: 'object',
|
|
518
528
|
properties: value.properties,
|
|
519
529
|
additionalProperties: false
|
|
520
530
|
};
|
|
531
|
+
} else if(value.type == 'one-to-many') {
|
|
532
|
+
withoutReferences[value.table] = {
|
|
533
|
+
type: 'array',
|
|
534
|
+
items: {
|
|
535
|
+
type: 'object',
|
|
536
|
+
properties: value.properties,
|
|
537
|
+
additionalProperties: false
|
|
538
|
+
}
|
|
539
|
+
};
|
|
521
540
|
}
|
|
522
541
|
}
|
|
523
542
|
}
|