yaml-admin-api 0.0.19 → 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 -34
- 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,10 +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 ==
|
|
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}`)
|
|
74
85
|
return parseValueByTypeCore(value, referenceField)
|
|
75
86
|
} else {
|
|
76
87
|
return parseValueByTypeCore(value, field)
|
|
@@ -79,7 +90,10 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
79
90
|
const parseValueByTypeCore = (value, field) => {
|
|
80
91
|
const { type } = field
|
|
81
92
|
if (type == 'integer')
|
|
82
|
-
|
|
93
|
+
if(value)
|
|
94
|
+
return parseInt(value)
|
|
95
|
+
else
|
|
96
|
+
return null
|
|
83
97
|
else if (type == 'string')
|
|
84
98
|
return value
|
|
85
99
|
else if (type == 'objectId')
|
|
@@ -111,6 +125,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
111
125
|
m[field.name] = await mediaToFront(m[field.name], field.private)
|
|
112
126
|
}
|
|
113
127
|
}
|
|
128
|
+
|
|
129
|
+
let apiGenerateFields = await makeApiGenerateFields(db, entity_name, yml_entity, yml, options, list)
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
const mediaKeyToFullUrl = async (key, private) => {
|
|
@@ -128,7 +144,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
128
144
|
|
|
129
145
|
const mediaToFront = async (media, private) => {
|
|
130
146
|
if (media && typeof media == 'string') {
|
|
131
|
-
|
|
147
|
+
const url = media
|
|
148
|
+
media = { src: url }
|
|
132
149
|
media.image_preview = await mediaKeyToFullUrl(url, private)
|
|
133
150
|
} else if (media && typeof media == 'object') {
|
|
134
151
|
let { image, video, src } = media
|
|
@@ -142,7 +159,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
142
159
|
|
|
143
160
|
|
|
144
161
|
//list
|
|
145
|
-
app.get(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
162
|
+
app.get(`/${entity_name}`, auth.isAuthenticated, asyncErrorHandler(async (req, res) => {
|
|
163
|
+
//검색 파라미터
|
|
146
164
|
var s = {};
|
|
147
165
|
var _sort = req.query._sort;
|
|
148
166
|
var _order = req.query._order;
|
|
@@ -162,15 +180,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
162
180
|
if (Array.isArray(q)) {
|
|
163
181
|
f[field.name] = { $in: q.map(v => parseValueByType(v, field)) };
|
|
164
182
|
} else {
|
|
165
|
-
if (search?.exact != false || field.type == 'integer')
|
|
183
|
+
if (search?.exact != false || field.type == 'integer') {
|
|
166
184
|
f[field.name] = parseValueByType(q, field)
|
|
167
|
-
else
|
|
185
|
+
} else
|
|
168
186
|
f[field.name] = { $regex: ".*" + q + ".*" };
|
|
169
187
|
}
|
|
188
|
+
} else {
|
|
189
|
+
//empty query - $exists : false
|
|
190
|
+
if(req.query[field.name] == '')
|
|
191
|
+
f[field.name] = null
|
|
170
192
|
}
|
|
171
193
|
})
|
|
172
194
|
|
|
173
195
|
//console.log('f', f)
|
|
196
|
+
//console.log('s', s)
|
|
174
197
|
|
|
175
198
|
var name = req.query.name;
|
|
176
199
|
if (name == null && req.query.q)
|
|
@@ -193,9 +216,10 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
193
216
|
|
|
194
217
|
//Custom list End
|
|
195
218
|
await addInfo(db, list)
|
|
219
|
+
|
|
196
220
|
res.header('X-Total-Count', count);
|
|
197
221
|
res.json(list);
|
|
198
|
-
});
|
|
222
|
+
}));
|
|
199
223
|
|
|
200
224
|
|
|
201
225
|
const constructEntity = async (req, entityId) => {
|
|
@@ -204,7 +228,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
204
228
|
if (entityId)
|
|
205
229
|
entity[key_field.name] = entityId
|
|
206
230
|
|
|
207
|
-
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 => {
|
|
208
245
|
if (!field.key)
|
|
209
246
|
entity[field.name] = req.body[field.name]
|
|
210
247
|
})
|
|
@@ -212,7 +249,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
212
249
|
|
|
213
250
|
let passwordFields = yml_entity.fields.filter(f => f.type == 'password').map(f => f.name)
|
|
214
251
|
for(let f of passwordFields) {
|
|
215
|
-
|
|
252
|
+
if(req.body[f])
|
|
253
|
+
entity[f] = await passwordEncrypt(req.body[f])
|
|
216
254
|
}
|
|
217
255
|
//Custom ConstructEntity Start
|
|
218
256
|
|
|
@@ -222,7 +260,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
222
260
|
};
|
|
223
261
|
|
|
224
262
|
//create
|
|
225
|
-
app.post(`/${entity_name}`, auth.isAuthenticated, async (req, res) => {
|
|
263
|
+
app.post(`/${entity_name}`, auth.isAuthenticated, asyncErrorHandler(async (req, res) => {
|
|
226
264
|
let entityId
|
|
227
265
|
if (key_field.autogenerate)
|
|
228
266
|
entityId = await generateKey()
|
|
@@ -234,7 +272,7 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
234
272
|
f[key_field.name] = entityId
|
|
235
273
|
let already = await db.collection(entity_name).findOne(f)
|
|
236
274
|
if (already)
|
|
237
|
-
|
|
275
|
+
throw new Error("duplicate key of [" + key_field.name + "] - [" + entityId + "]")
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
const entity = await constructEntity(req, entityId);
|
|
@@ -247,14 +285,14 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
247
285
|
|
|
248
286
|
var r = await db.collection(entity_name).insertOne(entity);
|
|
249
287
|
//Custom Create Tail Start
|
|
250
|
-
|
|
288
|
+
options?.listener?.entityCreated?.(db, entity_name, entity)
|
|
251
289
|
//Custom Create Tail End
|
|
252
290
|
|
|
253
291
|
const generatedId = entityId || r.insertedId
|
|
254
292
|
entity.id = (key_field.type == 'objectId') ? generatedId?.toString() : generatedId;
|
|
255
293
|
|
|
256
294
|
res.json(entity);
|
|
257
|
-
});
|
|
295
|
+
}));
|
|
258
296
|
|
|
259
297
|
|
|
260
298
|
//edit
|
|
@@ -275,18 +313,20 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
275
313
|
let f = {}
|
|
276
314
|
f[key_field.name] = entityId
|
|
277
315
|
|
|
278
|
-
for(let field of yml_entity.fields) {
|
|
279
|
-
if(['mp4', 'image', 'file'].includes(field.type)) {
|
|
316
|
+
for (let field of yml_entity.fields) {
|
|
317
|
+
if (['mp4', 'image', 'file'].includes(field.type)) {
|
|
280
318
|
let a = entity[field.name]
|
|
281
|
-
|
|
282
|
-
|
|
319
|
+
if (a) {
|
|
320
|
+
delete a.image_preview
|
|
321
|
+
delete a.video_preview
|
|
322
|
+
}
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
325
|
|
|
286
326
|
await db.collection(entity_name).updateOne(f, { $set: entity });
|
|
287
327
|
|
|
288
328
|
//Custom Create Tail Start
|
|
289
|
-
|
|
329
|
+
options?.listener?.entityUpdated?.(db, entity_name, entity)
|
|
290
330
|
//Custom Create Tail End
|
|
291
331
|
|
|
292
332
|
// Ensure React-Admin receives an `id` in the response
|
|
@@ -332,6 +372,8 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
332
372
|
else
|
|
333
373
|
await db.collection(entity_name).deleteOne(f);
|
|
334
374
|
|
|
375
|
+
options?.listener?.entityDeleted?.(db, entity_name, entity)
|
|
376
|
+
|
|
335
377
|
res.json(entity);
|
|
336
378
|
});
|
|
337
379
|
|
|
@@ -386,17 +428,17 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
386
428
|
let header = list[0]
|
|
387
429
|
list.shift();
|
|
388
430
|
|
|
389
|
-
let upsert = yml_entity.crud.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
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 => {
|
|
393
434
|
let original = yml_entity.fields.find(f => f.name == field.name)
|
|
394
|
-
|
|
435
|
+
return original
|
|
395
436
|
})
|
|
396
437
|
|
|
397
438
|
let key_field = yml_entity.fields.find(f => f.key)
|
|
398
439
|
let bulk = []
|
|
399
|
-
|
|
440
|
+
let opsMeta = []
|
|
441
|
+
for(let m of list) {
|
|
400
442
|
let f = {}
|
|
401
443
|
|
|
402
444
|
let m_obj = {}
|
|
@@ -405,18 +447,30 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
405
447
|
})
|
|
406
448
|
|
|
407
449
|
f[key_field.name] = getKeyFromEntity(m_obj)
|
|
408
|
-
if (!f[key_field.name])
|
|
409
|
-
|
|
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
|
+
|
|
410
458
|
let entity = {}
|
|
411
459
|
fields.forEach(field => {
|
|
412
|
-
if (field.type == 'integer')
|
|
460
|
+
if (field.type == 'integer') {
|
|
413
461
|
entity[field.name] = parseInt(m_obj[field.name])
|
|
414
|
-
else if (field.type == '
|
|
415
|
-
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] || '') + '')
|
|
416
466
|
else
|
|
417
|
-
entity[field.name] = m_obj[field.name] + ''
|
|
467
|
+
entity[field.name] = (m_obj[field.name] || '') + ''
|
|
418
468
|
})
|
|
419
469
|
|
|
470
|
+
delete entity[key_field.name]
|
|
471
|
+
|
|
472
|
+
const opIndex = bulk.length
|
|
473
|
+
opsMeta.push({ index: opIndex, key: f[key_field.name], entity })
|
|
420
474
|
bulk.push({
|
|
421
475
|
updateOne: {
|
|
422
476
|
filter: f,
|
|
@@ -424,10 +478,127 @@ const generateCrud = async ({ app, db, entity_name, yml_entity, yml, options })
|
|
|
424
478
|
upsert: upsert
|
|
425
479
|
}
|
|
426
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
|
|
427
586
|
})
|
|
428
587
|
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
}
|
|
431
602
|
})
|
|
432
603
|
}
|
|
433
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,
|