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 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.18",
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,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, 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)
74
- console.log('referenceField', referenceField)
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
- return parseInt(value)
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
- media= { src: url }
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.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 => {
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
- entity[f] = await passwordEncrypt(req.body[f])
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
- return res.status(400).json({ status: 400, statusText: 'error', message: "duplicate key [" + entityId + "]" });
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
- delete a.image_preview
283
- delete a.video_preview
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.list.import.upsert || true
391
-
392
- const fields = yml_entity.crud.list.import.fields.map(m => m)
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
- field.type = original.type
435
+ return original
396
436
  })
397
437
 
398
438
  let key_field = yml_entity.fields.find(f => f.key)
399
439
  let bulk = []
400
- list.map(m => {
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
- 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
+
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 == 'password')
416
- 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] || '') + '')
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
- let result = await db.collection('delivery').bulkWrite(bulk);
431
- 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
+ }
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 };
@@ -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,