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 CHANGED
@@ -209,7 +209,7 @@ Export response
209
209
  ```
210
210
  Import response
211
211
  ```json
212
- { "r": true, "msg": "Import success - <n> rows effected" }
212
+ { "r": true, "msg": "Import success - <n> new rows inserted" }
213
213
  ```
214
214
 
215
215
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaml-admin-api",
3
- "version": "0.0.19",
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
+ }
@@ -0,0 +1,12 @@
1
+ const makeMongoSortFromYml = (sort) => {
2
+ sort = sort || []
3
+ let r = {}
4
+ sort.map(m=>{
5
+ r[m.name] = m.desc == true ? -1 : 1
6
+ })
7
+ return r
8
+ }
9
+
10
+ module.exports = {
11
+ makeMongoSortFromYml
12
+ }
@@ -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, key_field.name)
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, reference_field } = field
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 == reference_field)
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
- return parseInt(value)
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
- media= { src: url }
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.forEach(field => {
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
- entity[f] = await passwordEncrypt(req.body[f])
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
- return res.status(400).json({ status: 400, statusText: 'error', message: "duplicate key [" + entityId + "]" });
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
- delete a.image_preview
282
- delete a.video_preview
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.list.import.upsert || true
390
-
391
- const fields = yml_entity.crud.list.import.fields.map(m => m)
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
- field.type = original.type
435
+ return original
395
436
  })
396
437
 
397
438
  let key_field = yml_entity.fields.find(f => f.key)
398
439
  let bulk = []
399
- list.map(m => {
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
- return
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 == 'password')
415
- entity[field.name] = passwordEncrypt(m_obj[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
- let result = await db.collection('delivery').bulkWrite(bulk);
430
- res.json({ r: true, msg: 'Import success - ' + result.upsertedCount + ' rows effected' });
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 };
@@ -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,