yaml-admin-api 0.0.18 → 0.0.20
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/README.md +1 -1
- package/package.json +2 -1
- package/src/crud/chart-api-generator.js +393 -0
- package/src/crud/crud-common.js +12 -0
- package/src/crud/entity-api-generator.js +205 -35
- package/src/index.js +2 -2
- package/src/yml-admin-api.js +3 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaml-admin-api",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "YAML Admin API package",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"bcryptjs": "^3.0.2",
|
|
20
20
|
"jsonwebtoken": "^9.0.2",
|
|
21
21
|
"moment": "^2.30.1",
|
|
22
|
+
"moment-timezone": "^0.6.0",
|
|
22
23
|
"mongodb": "^6.18.0",
|
|
23
24
|
"request": "^2.88.2",
|
|
24
25
|
"uuid": "^11.1.0",
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
const { withConfig } = require('../login/auth.js');
|
|
2
|
+
const moment = require('moment-timezone');
|
|
3
|
+
const { makeMongoSortFromYml } = require('./crud-common.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
|
|
7
|
+
* @param {*} app
|
|
8
|
+
* @param {*} db
|
|
9
|
+
* @param {*} yml
|
|
10
|
+
*/
|
|
11
|
+
const generateChartApi = async (app, db, yml) => {
|
|
12
|
+
const { front } = yml;
|
|
13
|
+
const dashboard = front?.dashboard;
|
|
14
|
+
if (!dashboard)
|
|
15
|
+
return;
|
|
16
|
+
|
|
17
|
+
const chartComponents = dashboard.filter(m => m.component === 'chart');
|
|
18
|
+
if (chartComponents.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
|
|
21
|
+
const auth = withConfig({ db, jwt_secret: yml.login["jwt-secret"] });
|
|
22
|
+
|
|
23
|
+
const createChartDataTypeDate = async (chart, {from_date}) => {
|
|
24
|
+
const r = {
|
|
25
|
+
options: {
|
|
26
|
+
chart: { id: chart.id },
|
|
27
|
+
xaxis: { categories: [] }
|
|
28
|
+
},
|
|
29
|
+
colors: [],
|
|
30
|
+
series: []
|
|
31
|
+
}
|
|
32
|
+
const { x, y, relation } = chart;
|
|
33
|
+
if (y && Array.isArray(y.series)) {
|
|
34
|
+
const definedColors = y.series.map(s => s && s.color).filter(Boolean);
|
|
35
|
+
if (definedColors.length > 0) {
|
|
36
|
+
r.options.colors = definedColors;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { field, entity : entity_x, format, gap, limit, desc, timezone } = x;
|
|
41
|
+
const { entity : entity_y } = y;
|
|
42
|
+
|
|
43
|
+
for (const s of y.series) {
|
|
44
|
+
const { label } = s;
|
|
45
|
+
const match = evaluateIfToMatch(s['if']);
|
|
46
|
+
|
|
47
|
+
let lookup_list = []
|
|
48
|
+
if (relation) {
|
|
49
|
+
let x_chain = relation_chain_y_to_x.find(f=>f.entity === entity_x)
|
|
50
|
+
let lookup = {
|
|
51
|
+
from: entity_y,
|
|
52
|
+
let: { root_x_key: `$${x_chain.match_from}` },
|
|
53
|
+
pipeline: [],
|
|
54
|
+
as: entity_y,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for(let i=0;i<relation_chain_y_to_x.length;i++) {
|
|
58
|
+
const m = relation_chain_y_to_x[i]
|
|
59
|
+
lookup.pipeline.push({
|
|
60
|
+
$lookup: {
|
|
61
|
+
from: m.entity,
|
|
62
|
+
localField: m.match_from,
|
|
63
|
+
foreignField: m.match,
|
|
64
|
+
as: m.entity
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
lookup.pipeline.push({ $unwind: `$${m.entity}` })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lookup.pipeline.push({ $match: { $expr: { $eq: [`$${x_chain.match}`, `$$root_x_key`] } } },)
|
|
71
|
+
lookup.pipeline.push({ $count: 'n' })
|
|
72
|
+
lookup_list.push({$lookup:lookup})
|
|
73
|
+
lookup_list.push({
|
|
74
|
+
$unwind: {
|
|
75
|
+
path: `$${entity_y}`,
|
|
76
|
+
preserveNullAndEmptyArrays:true,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let group_list = []
|
|
82
|
+
group_list.push({
|
|
83
|
+
$group: {
|
|
84
|
+
_id: {
|
|
85
|
+
$dateTrunc: {
|
|
86
|
+
date: `$${field}`,
|
|
87
|
+
unit: gap,
|
|
88
|
+
timezone: timezone
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"count": { "$sum": 1 }
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
let a = [
|
|
96
|
+
{ $match: {server_id:148, parent_id:null}},
|
|
97
|
+
...lookup_list,
|
|
98
|
+
{ $match: match},
|
|
99
|
+
...group_list,
|
|
100
|
+
{ $sort: { _id: -1 } },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
if(limit)
|
|
104
|
+
a.push({ $limit: limit })
|
|
105
|
+
|
|
106
|
+
const list = await db.collection(entity_x).aggregate(a).toArray();
|
|
107
|
+
|
|
108
|
+
list.map(m => {
|
|
109
|
+
if (format)
|
|
110
|
+
return moment.tz(m._id, timezone).format(format);
|
|
111
|
+
else
|
|
112
|
+
return m._id;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if(!from_date)
|
|
116
|
+
from_date = moment().tz(timezone).format('YYYYMMDD');
|
|
117
|
+
|
|
118
|
+
r.options.xaxis.categories = []
|
|
119
|
+
let cmoment = moment.tz(from_date, timezone);
|
|
120
|
+
for(let i=0; i<limit; i++) {
|
|
121
|
+
r.options.xaxis.categories.push(cmoment.format(format));
|
|
122
|
+
cmoment.add(-1, gap)
|
|
123
|
+
}
|
|
124
|
+
if(!desc)
|
|
125
|
+
r.options.xaxis.categories.reverse();
|
|
126
|
+
r.series.push({ name: label, data: r.options.xaxis.categories.map(m=>{
|
|
127
|
+
return list.find(l=>moment.tz(l._id, timezone).format(format) === m)?.count || 0;
|
|
128
|
+
})});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return r
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const createChartDataTypeField = async (chart) => {
|
|
135
|
+
const r = {
|
|
136
|
+
options: {
|
|
137
|
+
chart: { id: chart.id },
|
|
138
|
+
xaxis: { categories: [] }
|
|
139
|
+
},
|
|
140
|
+
colors: [],
|
|
141
|
+
series: []
|
|
142
|
+
}
|
|
143
|
+
const { x, y, relation } = chart;
|
|
144
|
+
if (y && Array.isArray(y.series)) {
|
|
145
|
+
const definedColors = y.series.map(s => s && s.color).filter(Boolean);
|
|
146
|
+
if (definedColors.length > 0) {
|
|
147
|
+
r.options.colors = definedColors;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { field, entity : entity_x, format, gap, limit, desc, sort } = x;
|
|
152
|
+
const { entity : entity_y } = y;
|
|
153
|
+
|
|
154
|
+
for (const s of y.series) {
|
|
155
|
+
const { label } = s;
|
|
156
|
+
|
|
157
|
+
let match_list = []
|
|
158
|
+
if(s['if'] && !relation)
|
|
159
|
+
match_list.push({ $match: evaluateIfToMatch(s['if'])})
|
|
160
|
+
|
|
161
|
+
let lookup_list = []
|
|
162
|
+
if (relation) {
|
|
163
|
+
if(!relation.chain)
|
|
164
|
+
throw new Error('relation.chain is required');
|
|
165
|
+
if(!relation.match)
|
|
166
|
+
throw new Error('relation.match is required');
|
|
167
|
+
|
|
168
|
+
let lookup = {
|
|
169
|
+
from: entity_y,
|
|
170
|
+
let: { root_x_key: `$${relation.match.x}` },
|
|
171
|
+
pipeline: [],
|
|
172
|
+
as: entity_y,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for(let i=0;i<relation.chain.length;i++) {
|
|
176
|
+
const m = relation.chain[i]
|
|
177
|
+
lookup.pipeline.push({
|
|
178
|
+
$lookup: {
|
|
179
|
+
from: m.entity,
|
|
180
|
+
localField: m.match_from,
|
|
181
|
+
foreignField: m.match,
|
|
182
|
+
as: m.entity
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
lookup.pipeline.push({ $unwind: `$${m.entity}` })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if(s['if'])
|
|
190
|
+
lookup.pipeline.push({ $match: evaluateIfToMatch(s['if'])})
|
|
191
|
+
|
|
192
|
+
lookup.pipeline.push({ $match: { $expr: { $eq: [`$${relation.match.with}`, `$$root_x_key`] } } },)
|
|
193
|
+
lookup.pipeline.push({ $count: 'n' })
|
|
194
|
+
lookup_list.push({$lookup:lookup})
|
|
195
|
+
lookup_list.push({
|
|
196
|
+
$unwind: {
|
|
197
|
+
path: `$${entity_y}`,
|
|
198
|
+
preserveNullAndEmptyArrays:true,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let group_sort_field = {}
|
|
204
|
+
sort && sort.map(m=>{
|
|
205
|
+
group_sort_field[m.name] = {
|
|
206
|
+
$max: `$${m.name}`
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
let group_list = []
|
|
211
|
+
if(relation) {
|
|
212
|
+
group_list.push({
|
|
213
|
+
$group: {
|
|
214
|
+
_id: `$${field}`,
|
|
215
|
+
"count": { "$sum": `$${entity_y}.n` },
|
|
216
|
+
...group_sort_field
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
} else {
|
|
220
|
+
group_list.push({
|
|
221
|
+
$group: {
|
|
222
|
+
_id: `$${field}`,
|
|
223
|
+
"count": { "$sum": 1 },
|
|
224
|
+
...group_sort_field
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let a = [
|
|
230
|
+
{ $match: {server_id:148, parent_id:null}},
|
|
231
|
+
...lookup_list,
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
a.push(...group_list)
|
|
235
|
+
|
|
236
|
+
if(sort)
|
|
237
|
+
a.push({ $sort: makeMongoSortFromYml(sort) })
|
|
238
|
+
|
|
239
|
+
if(limit)
|
|
240
|
+
a.push({ $limit: limit })
|
|
241
|
+
|
|
242
|
+
//debug
|
|
243
|
+
if(chart.debug)
|
|
244
|
+
console.log('a', entity_x, JSON.stringify(a, null, 2))
|
|
245
|
+
|
|
246
|
+
const list = await db.collection(entity_x).aggregate(a).toArray();
|
|
247
|
+
|
|
248
|
+
r.options.xaxis.categories = list.map(m => m._id);
|
|
249
|
+
r.series.push({ name: label, data: list.map(m => m.count) });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return r
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const chart of chartComponents) {
|
|
256
|
+
const { id } = chart;
|
|
257
|
+
console.log('generateChartApi', chart.id)
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
return sample {
|
|
261
|
+
options: {
|
|
262
|
+
chart: {
|
|
263
|
+
id: "basic-bar"
|
|
264
|
+
},
|
|
265
|
+
xaxis: {
|
|
266
|
+
categories: [1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
series: [
|
|
270
|
+
{
|
|
271
|
+
name: "series-1",
|
|
272
|
+
data: [30, 40, 45, 50, 49, 60, 70, 91]
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "series-2",
|
|
276
|
+
data: [30, 40, 45, 50, 49, 60, 70, 91]
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
*/
|
|
281
|
+
app.get(`/api/chart/${id}`, auth.isAuthenticated, async (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const { x } = chart;
|
|
284
|
+
let r
|
|
285
|
+
if (x.type == 'date') {
|
|
286
|
+
let {from_date} = req.query //YYYYMMDD
|
|
287
|
+
r = await createChartDataTypeDate(chart, {from_date});
|
|
288
|
+
} else if(x.type == 'field') {
|
|
289
|
+
r = await createChartDataTypeField(chart);
|
|
290
|
+
} else {
|
|
291
|
+
throw new Error('x.type is not date or field');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
res.json(r);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error(e);
|
|
297
|
+
res.status(400).json({ r: false, msg: e.message });
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 'lock==true' or 'lock!=true' to mongodb match format like {lock:true} , {lock:{$ne:true}}
|
|
304
|
+
* @param {*} expression
|
|
305
|
+
* @returns
|
|
306
|
+
*/
|
|
307
|
+
function evaluateIfToMatch(expression) {
|
|
308
|
+
if (!expression || typeof expression !== 'string') return {};
|
|
309
|
+
const exp = expression.trim();
|
|
310
|
+
|
|
311
|
+
// Support shorthand truthy checks: "flag" => { flag: true }, "!flag" => { flag: { $ne: true } }
|
|
312
|
+
if (!/[=!<>]/.test(exp)) {
|
|
313
|
+
if (exp.startsWith('!')) {
|
|
314
|
+
const field = exp.substring(1).trim();
|
|
315
|
+
if (!field) return {};
|
|
316
|
+
return { [field]: { $ne: true } };
|
|
317
|
+
}
|
|
318
|
+
return { [exp]: true };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const match = exp.match(/^(.+?)(==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
322
|
+
if (!match) return {};
|
|
323
|
+
|
|
324
|
+
const field = match[1].trim();
|
|
325
|
+
const op = match[2];
|
|
326
|
+
const rightRaw = match[3].trim();
|
|
327
|
+
|
|
328
|
+
const parseLiteral = (raw) => {
|
|
329
|
+
if (raw === undefined || raw === null) return raw;
|
|
330
|
+
let v = raw.trim();
|
|
331
|
+
|
|
332
|
+
// strip quotes if wrapped
|
|
333
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith('\'') && v.endsWith('\''))) {
|
|
334
|
+
return v.substring(1, v.length - 1);
|
|
335
|
+
}
|
|
336
|
+
if (v.toLowerCase() === 'true') return true;
|
|
337
|
+
if (v.toLowerCase() === 'false') return false;
|
|
338
|
+
if (v.toLowerCase() === 'null') return null;
|
|
339
|
+
|
|
340
|
+
// Array or object JSON
|
|
341
|
+
if (v.startsWith('[') || v.startsWith('{')) {
|
|
342
|
+
try {
|
|
343
|
+
// allow single-quoted json by converting to double quotes conservatively
|
|
344
|
+
const normalized = v.replace(/'([^']*)'/g, '"$1"');
|
|
345
|
+
return JSON.parse(normalized);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
// fallthrough
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// number
|
|
352
|
+
const num = Number(v);
|
|
353
|
+
if (!Number.isNaN(num) && v !== '') return num;
|
|
354
|
+
|
|
355
|
+
// ISO date
|
|
356
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
357
|
+
if (isoDateRegex.test(v)) {
|
|
358
|
+
const d = new Date(v);
|
|
359
|
+
if (!isNaN(d.getTime())) return d;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return v;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const value = parseLiteral(rightRaw);
|
|
366
|
+
|
|
367
|
+
switch (op) {
|
|
368
|
+
case '==':
|
|
369
|
+
if (Array.isArray(value)) return { [field]: { $in: value } };
|
|
370
|
+
return { [field]: value };
|
|
371
|
+
case '!=':
|
|
372
|
+
if (Array.isArray(value)) return { [field]: { $nin: value } };
|
|
373
|
+
return { [field]: { $ne: value } };
|
|
374
|
+
case '>':
|
|
375
|
+
return { [field]: { $gt: value } };
|
|
376
|
+
case '>=':
|
|
377
|
+
return { [field]: { $gte: value } };
|
|
378
|
+
case '<':
|
|
379
|
+
return { [field]: { $lt: value } };
|
|
380
|
+
case '<=':
|
|
381
|
+
return { [field]: { $lte: value } };
|
|
382
|
+
default:
|
|
383
|
+
return {};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getPath(obj, path) {
|
|
388
|
+
return path.split('.').reduce((acc, k) => (acc && acc[k] !== undefined ? acc[k] : undefined), obj);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
generateChartApi
|
|
393
|
+
}
|
|
@@ -8,6 +8,14 @@ const XLSX = require('xlsx');
|
|
|
8
8
|
const moment = require('moment');
|
|
9
9
|
const { withConfigLocal } = require('../upload/localUpload.js');
|
|
10
10
|
const { withConfigS3 } = require('../upload/s3Upload.js');
|
|
11
|
+
const { makeMongoSortFromYml } = require('./crud-common.js');
|
|
12
|
+
|
|
13
|
+
const asyncErrorHandler = (fn) => (req, res, next) => {
|
|
14
|
+
return Promise.resolve(fn(req, res, next)).catch(async e=>{
|
|
15
|
+
console.error(e);
|
|
16
|
+
res.status(400).json({ status: 400, statusText: 'error', message: e.message })
|
|
17
|
+
});
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options }) => {
|
|
13
21
|
|
|
@@ -16,6 +24,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
16
24
|
const api_host = yml["api-host"].uri;
|
|
17
25
|
let isS3 = yml.upload.s3
|
|
18
26
|
let host_image = isS3 ? yml.upload.s3.base_url : yml.upload.local.base_url
|
|
27
|
+
|
|
19
28
|
const uploader = yml.upload.s3 ? withConfigS3({
|
|
20
29
|
access_key_id: yml.upload.s3.access_key_id,
|
|
21
30
|
secret_access_key: yml.upload.s3.secret_access_key,
|
|
@@ -43,7 +52,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
43
52
|
|
|
44
53
|
const generateKey = async () => {
|
|
45
54
|
if (key_field.type == 'integer')
|
|
46
|
-
return await genEntityIdWithKey(db,
|
|
55
|
+
return await genEntityIdWithKey(db, entity_name)
|
|
47
56
|
else if (key_field.type == 'string')
|
|
48
57
|
return uuidv4()
|
|
49
58
|
return null
|
|
@@ -67,11 +76,12 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
const parseValueByType = (value, field) => {
|
|
70
|
-
const { type, reference_entity,
|
|
79
|
+
const { type, reference_entity, reference_match } = field
|
|
71
80
|
if (type == 'reference') {
|
|
72
81
|
const referenceEntity = yml.entity[reference_entity]
|
|
73
|
-
const referenceField = referenceEntity.fields.find(f => f.name ==
|
|
74
|
-
|
|
82
|
+
const referenceField = referenceEntity.fields.find(f => f.name == reference_match)
|
|
83
|
+
if(!referenceField)
|
|
84
|
+
throw new Error(`Reference field ${reference_match} not found in ${reference_entity}`)
|
|
75
85
|
return parseValueByTypeCore(value, referenceField)
|
|
76
86
|
} else {
|
|
77
87
|
return parseValueByTypeCore(value, field)
|
|
@@ -80,7 +90,10 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
80
90
|
const parseValueByTypeCore = (value, field) => {
|
|
81
91
|
const { type } = field
|
|
82
92
|
if (type == 'integer')
|
|
83
|
-
|
|
93
|
+
if(value)
|
|
94
|
+
return parseInt(value)
|
|
95
|
+
else
|
|
96
|
+
return null
|
|
84
97
|
else if (type == 'string')
|
|
85
98
|
return value
|
|
86
99
|
else if (type == 'objectId')
|
|
@@ -112,6 +125,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
112
125
|
m[field.name] = await mediaToFront(m[field.name], field.private)
|
|
113
126
|
}
|
|
114
127
|
}
|
|
128
|
+
|
|
129
|
+
let apiGenerateFields = await makeApiGenerateFields(db, entity_name, yml_entity, yml, options, list)
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
const mediaKeyToFullUrl = async (key, private) => {
|
|
@@ -129,7 +144,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
129
144
|
|
|
130
145
|
const mediaToFront = async (media, private) => {
|
|
131
146
|
if (media && typeof media == 'string') {
|
|
132
|
-
|
|
147
|
+
const url = media
|
|
148
|
+
media = { src: url }
|
|
133
149
|
media.image_preview = await mediaKeyToFullUrl(url, private)
|
|
134
150
|
} else if (media && typeof media == 'object') {
|
|
135
151
|
let { image, video, src } = media
|
|
@@ -143,7 +159,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
143
159
|
|
|
144
160
|
|
|
145
161
|
//list
|
|
146
|
-
app.get(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
162
|
+
app.get(`/${entity_name}`, auth.isAuthenticated, asyncErrorHandler(async (req, res) => {
|
|
163
|
+
//검색 파라미터
|
|
147
164
|
var s = {};
|
|
148
165
|
var _sort = req.query._sort;
|
|
149
166
|
var _order = req.query._order;
|
|
@@ -163,15 +180,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
163
180
|
if (Array.isArray(q)) {
|
|
164
181
|
f[field.name] = { $in: q.map(v => parseValueByType(v, field)) };
|
|
165
182
|
} else {
|
|
166
|
-
if (search?.exact != false || field.type == 'integer')
|
|
183
|
+
if (search?.exact != false || field.type == 'integer') {
|
|
167
184
|
f[field.name] = parseValueByType(q, field)
|
|
168
|
-
else
|
|
185
|
+
} else
|
|
169
186
|
f[field.name] = { $regex: ".*" + q + ".*" };
|
|
170
187
|
}
|
|
188
|
+
} else {
|
|
189
|
+
//empty query - $exists : false
|
|
190
|
+
if(req.query[field.name] == '')
|
|
191
|
+
f[field.name] = null
|
|
171
192
|
}
|
|
172
193
|
})
|
|
173
194
|
|
|
174
195
|
//console.log('f', f)
|
|
196
|
+
//console.log('s', s)
|
|
175
197
|
|
|
176
198
|
var name = req.query.name;
|
|
177
199
|
if (name == null && req.query.q)
|
|
@@ -194,9 +216,10 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
194
216
|
|
|
195
217
|
//Custom list End
|
|
196
218
|
await addInfo(db, list)
|
|
219
|
+
|
|
197
220
|
res.header('X-Total-Count', count);
|
|
198
221
|
res.json(list);
|
|
199
|
-
});
|
|
222
|
+
}));
|
|
200
223
|
|
|
201
224
|
|
|
202
225
|
const constructEntity = async (req, entityId) => {
|
|
@@ -205,7 +228,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
205
228
|
if (entityId)
|
|
206
229
|
entity[key_field.name] = entityId
|
|
207
230
|
|
|
208
|
-
yml_entity.fields
|
|
231
|
+
yml_entity.fields
|
|
232
|
+
.filter(f => !['password', 'length'].includes(f.type))
|
|
233
|
+
//exclude field by api_generate
|
|
234
|
+
.filter(f => {
|
|
235
|
+
if(!yml_entity.api_generate)
|
|
236
|
+
return true;
|
|
237
|
+
if(yml_entity.api_generate[f.name])
|
|
238
|
+
return false;
|
|
239
|
+
if(f.name.includes('.') && yml_entity.api_generate[f.name.split('.')[0]]) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
})
|
|
244
|
+
.forEach(field => {
|
|
209
245
|
if (!field.key)
|
|
210
246
|
entity[field.name] = req.body[field.name]
|
|
211
247
|
})
|
|
@@ -213,7 +249,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
213
249
|
|
|
214
250
|
let passwordFields = yml_entity.fields.filter(f => f.type == 'password').map(f => f.name)
|
|
215
251
|
for(let f of passwordFields) {
|
|
216
|
-
|
|
252
|
+
if(req.body[f])
|
|
253
|
+
entity[f] = await passwordEncrypt(req.body[f])
|
|
217
254
|
}
|
|
218
255
|
//Custom ConstructEntity Start
|
|
219
256
|
|
|
@@ -223,7 +260,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
223
260
|
};
|
|
224
261
|
|
|
225
262
|
//create
|
|
226
|
-
app.post(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
263
|
+
app.post(`/${entity_name}`, auth.isAuthenticated, asyncErrorHandler(async (req, res) => {
|
|
227
264
|
let entityId
|
|
228
265
|
if (key_field.autogenerate)
|
|
229
266
|
entityId = await generateKey()
|
|
@@ -235,7 +272,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
235
272
|
f[key_field.name] = entityId
|
|
236
273
|
let already = await db.collection(entity_name).findOne(f)
|
|
237
274
|
if (already)
|
|
238
|
-
|
|
275
|
+
throw new Error("duplicate key of [" + key_field.name + "] - [" + entityId + "]")
|
|
239
276
|
}
|
|
240
277
|
|
|
241
278
|
const entity = await constructEntity(req, entityId);
|
|
@@ -248,14 +285,14 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
248
285
|
|
|
249
286
|
var r = await db.collection(entity_name).insertOne(entity);
|
|
250
287
|
//Custom Create Tail Start
|
|
251
|
-
|
|
288
|
+
options?.listener?.entityCreated?.(db, entity_name, entity)
|
|
252
289
|
//Custom Create Tail End
|
|
253
290
|
|
|
254
291
|
const generatedId = entityId || r.insertedId
|
|
255
292
|
entity.id = (key_field.type == 'objectId') ? generatedId?.toString() : generatedId;
|
|
256
293
|
|
|
257
294
|
res.json(entity);
|
|
258
|
-
});
|
|
295
|
+
}));
|
|
259
296
|
|
|
260
297
|
|
|
261
298
|
//edit
|
|
@@ -276,18 +313,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
276
313
|
let f = {}
|
|
277
314
|
f[key_field.name] = entityId
|
|
278
315
|
|
|
279
|
-
for(let field of yml_entity.fields) {
|
|
280
|
-
if(['mp4', 'image', 'file'].includes(field.type)) {
|
|
316
|
+
for (let field of yml_entity.fields) {
|
|
317
|
+
if (['mp4', 'image', 'file'].includes(field.type)) {
|
|
281
318
|
let a = entity[field.name]
|
|
282
|
-
|
|
283
|
-
|
|
319
|
+
if (a) {
|
|
320
|
+
delete a.image_preview
|
|
321
|
+
delete a.video_preview
|
|
322
|
+
}
|
|
284
323
|
}
|
|
285
324
|
}
|
|
286
325
|
|
|
287
326
|
await db.collection(entity_name).updateOne(f, { $set: entity });
|
|
288
327
|
|
|
289
328
|
//Custom Create Tail Start
|
|
290
|
-
|
|
329
|
+
options?.listener?.entityUpdated?.(db, entity_name, entity)
|
|
291
330
|
//Custom Create Tail End
|
|
292
331
|
|
|
293
332
|
// Ensure React-Admin receives an `id` in the response
|
|
@@ -333,6 +372,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
333
372
|
else
|
|
334
373
|
await db.collection(entity_name).deleteOne(f);
|
|
335
374
|
|
|
375
|
+
options?.listener?.entityDeleted?.(db, entity_name, entity)
|
|
376
|
+
|
|
336
377
|
res.json(entity);
|
|
337
378
|
});
|
|
338
379
|
|
|
@@ -387,17 +428,17 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
387
428
|
let header = list[0]
|
|
388
429
|
list.shift();
|
|
389
430
|
|
|
390
|
-
let upsert = yml_entity.crud.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
fields.map(field => {
|
|
431
|
+
let upsert = yml_entity.crud.import.upsert || true
|
|
432
|
+
let fields = yml_entity.crud.import.fields.map(m => m)
|
|
433
|
+
fields = fields.map(field => {
|
|
394
434
|
let original = yml_entity.fields.find(f => f.name == field.name)
|
|
395
|
-
|
|
435
|
+
return original
|
|
396
436
|
})
|
|
397
437
|
|
|
398
438
|
let key_field = yml_entity.fields.find(f => f.key)
|
|
399
439
|
let bulk = []
|
|
400
|
-
|
|
440
|
+
let opsMeta = []
|
|
441
|
+
for(let m of list) {
|
|
401
442
|
let f = {}
|
|
402
443
|
|
|
403
444
|
let m_obj = {}
|
|
@@ -406,18 +447,30 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
406
447
|
})
|
|
407
448
|
|
|
408
449
|
f[key_field.name] = getKeyFromEntity(m_obj)
|
|
409
|
-
if (!f[key_field.name])
|
|
410
|
-
|
|
450
|
+
if (!f[key_field.name]) {
|
|
451
|
+
if(key_field.autogenerate) {
|
|
452
|
+
f[key_field.name] = await generateKey()
|
|
453
|
+
} else {
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
411
458
|
let entity = {}
|
|
412
459
|
fields.forEach(field => {
|
|
413
|
-
if (field.type == 'integer')
|
|
460
|
+
if (field.type == 'integer') {
|
|
414
461
|
entity[field.name] = parseInt(m_obj[field.name])
|
|
415
|
-
else if (field.type == '
|
|
416
|
-
entity[field.name] =
|
|
462
|
+
} else if (field.type == 'reference') {
|
|
463
|
+
entity[field.name] = parseValueByType(m_obj[field.name], field)
|
|
464
|
+
} else if (field.type == 'password')
|
|
465
|
+
entity[field.name] = passwordEncrypt((m_obj[field.name] || '') + '')
|
|
417
466
|
else
|
|
418
|
-
entity[field.name] = m_obj[field.name] + ''
|
|
467
|
+
entity[field.name] = (m_obj[field.name] || '') + ''
|
|
419
468
|
})
|
|
420
469
|
|
|
470
|
+
delete entity[key_field.name]
|
|
471
|
+
|
|
472
|
+
const opIndex = bulk.length
|
|
473
|
+
opsMeta.push({ index: opIndex, key: f[key_field.name], entity })
|
|
421
474
|
bulk.push({
|
|
422
475
|
updateOne: {
|
|
423
476
|
filter: f,
|
|
@@ -425,10 +478,127 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
425
478
|
upsert: upsert
|
|
426
479
|
}
|
|
427
480
|
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let result = await db.collection(entity_name).bulkWrite(bulk);
|
|
484
|
+
//result에서 update entity와 created entity list로 추출 해서 options?.listener?.entityCreated?.(entity_name, createdEntity)와 options?.listener?.entityUpdated?.(entity_name, updateEntity) 호출
|
|
485
|
+
try {
|
|
486
|
+
const upsertIndexToId = new Map()
|
|
487
|
+
if (result && result.upsertedIds) {
|
|
488
|
+
Object.keys(result.upsertedIds).forEach(k => {
|
|
489
|
+
const idx = parseInt(k)
|
|
490
|
+
upsertIndexToId.set(idx, result.upsertedIds[k])
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
if (result && typeof result.getUpsertedIds === 'function') {
|
|
494
|
+
const arr = result.getUpsertedIds()
|
|
495
|
+
if (Array.isArray(arr)) {
|
|
496
|
+
arr.forEach(({ index, _id }) => {
|
|
497
|
+
upsertIndexToId.set(index, _id)
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const createdList = []
|
|
503
|
+
const updatedList = []
|
|
504
|
+
for (let meta of opsMeta) {
|
|
505
|
+
if (upsertIndexToId.has(meta.index)) createdList.push(meta)
|
|
506
|
+
else updatedList.push(meta)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// created
|
|
510
|
+
for (let { key, entity } of createdList) {
|
|
511
|
+
const createdEntity = { ...entity }
|
|
512
|
+
createdEntity[key_field.name] = key
|
|
513
|
+
createdEntity.id = (key_field.type == 'objectId') ? (key && key.toString ? key.toString() : key) : key
|
|
514
|
+
options?.listener?.entityCreated?.(db, entity_name, createdEntity)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// updated (include matched-but-not-modified as existing)
|
|
518
|
+
for (let { key, entity } of updatedList) {
|
|
519
|
+
const updateEntity = { ...entity }
|
|
520
|
+
updateEntity[key_field.name] = key
|
|
521
|
+
updateEntity.id = (key_field.type == 'objectId') ? (key && key.toString ? key.toString() : key) : key
|
|
522
|
+
options?.listener?.entityUpdated?.(db, entity_name, updateEntity)
|
|
523
|
+
}
|
|
524
|
+
} catch (e) {
|
|
525
|
+
// ignore listener errors to not break import
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
res.json({ r: true, msg: 'Import success - ' + result.upsertedCount + ' new rows inserted' });
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* ex)
|
|
536
|
+
* data_list
|
|
537
|
+
* place: [{
|
|
538
|
+
* id:1
|
|
539
|
+
* }]
|
|
540
|
+
*
|
|
541
|
+
* path = "place.id"
|
|
542
|
+
* @param {*} obj
|
|
543
|
+
* @param {*} path
|
|
544
|
+
* @returns
|
|
545
|
+
*/
|
|
546
|
+
const matchPathInObject = (obj, path) => {
|
|
547
|
+
let r = obj[path]
|
|
548
|
+
if(!r && path.includes('.')) {
|
|
549
|
+
const parts = path.split('.')
|
|
550
|
+
let c = obj
|
|
551
|
+
for(let part of parts) {
|
|
552
|
+
c = c[part]
|
|
553
|
+
if(!c)
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
r = c
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return r
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const makeApiGenerateFields = async (db, entity_name, yml_entity, yml, options, data_list) => {
|
|
563
|
+
const apiGenerate = yml_entity.api_generate
|
|
564
|
+
if(!apiGenerate)
|
|
565
|
+
return;
|
|
566
|
+
for(let key in apiGenerate) {
|
|
567
|
+
|
|
568
|
+
const apiGenerateItem = apiGenerate[key]
|
|
569
|
+
let { entity, field, fields, match, sort, limit, single, match_from } = apiGenerateItem
|
|
570
|
+
|
|
571
|
+
sort = sort || []
|
|
572
|
+
sort = makeMongoSortFromYml(sort)
|
|
573
|
+
limit = limit || 1000
|
|
574
|
+
|
|
575
|
+
let match_from_list = data_list.map(m=>matchPathInObject(m, match_from))
|
|
576
|
+
|
|
577
|
+
match_from_list = match_from_list.filter(m=>m)
|
|
578
|
+
const f = { [match]: {$in:match_from_list} }
|
|
579
|
+
const projection = {[match]:1}
|
|
580
|
+
|
|
581
|
+
if(field)
|
|
582
|
+
projection[field] = 1
|
|
583
|
+
else
|
|
584
|
+
fields.map(m=>{
|
|
585
|
+
projection[m.name] = 1
|
|
428
586
|
})
|
|
429
587
|
|
|
430
|
-
|
|
431
|
-
|
|
588
|
+
const result = await db.collection(entity).find(f).project(projection).sort(sort).limit(limit).toArray()
|
|
589
|
+
data_list.map(m=>{
|
|
590
|
+
let found = result.filter(f=>matchPathInObject(f, match) === matchPathInObject(m, match_from))
|
|
591
|
+
if(single) {
|
|
592
|
+
if(field)
|
|
593
|
+
m[key] = found.length > 0 ? found[0][field] : null
|
|
594
|
+
else
|
|
595
|
+
m[key] = found.length > 0 ? found[0] : null
|
|
596
|
+
} else {
|
|
597
|
+
if(field)
|
|
598
|
+
m[key] = found.map(f=>f[field])
|
|
599
|
+
else
|
|
600
|
+
m[key] = found
|
|
601
|
+
}
|
|
432
602
|
})
|
|
433
603
|
}
|
|
434
604
|
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
const registerRoutes = require('./yml-admin-api.js');
|
|
2
|
-
|
|
3
|
-
module.exports = { registerRoutes };
|
|
2
|
+
const { genEntityIdWithKey } = require('./common/util.js');
|
|
3
|
+
module.exports = { registerRoutes, genEntityIdWithKey };
|
package/src/yml-admin-api.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs').promises;
|
|
|
3
3
|
const yaml = require('yaml');
|
|
4
4
|
const { generateEntityApi } = require('./crud/entity-api-generator');
|
|
5
5
|
const { generateLoginApi } = require('./crud/login-api-generator');
|
|
6
|
+
const { generateChartApi } = require('./crud/chart-api-generator');
|
|
6
7
|
const { withConfig } = require('./login/auth.js');
|
|
7
8
|
const { generateUploadApi } = require('./upload/upload-api-generator');
|
|
8
9
|
|
|
@@ -47,6 +48,8 @@ async function registerRoutes(app, options = {}) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
await generateLoginApi(app, db, yml)
|
|
51
|
+
await generateChartApi(app, db, yml)
|
|
52
|
+
|
|
50
53
|
entity && Object.keys(entity).forEach(async (entity_name) => {
|
|
51
54
|
await generateEntityApi({
|
|
52
55
|
app, db,
|