@voxgig/apidef 2.0.0 → 2.1.0

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.
Files changed (48) hide show
  1. package/dist/apidef.js +14 -20
  2. package/dist/apidef.js.map +1 -1
  3. package/dist/builder/flow/flowHeuristic01.js +37 -9
  4. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  5. package/dist/builder/flow.js +3 -3
  6. package/dist/builder/flow.js.map +1 -1
  7. package/dist/guide/heuristic01.js +197 -117
  8. package/dist/guide/heuristic01.js.map +1 -1
  9. package/dist/guide.js +37 -11
  10. package/dist/guide.js.map +1 -1
  11. package/dist/parse.d.ts +2 -1
  12. package/dist/parse.js +81 -3
  13. package/dist/parse.js.map +1 -1
  14. package/dist/transform/clean.d.ts +3 -0
  15. package/dist/transform/clean.js +16 -0
  16. package/dist/transform/clean.js.map +1 -0
  17. package/dist/transform/entity.js +21 -2
  18. package/dist/transform/entity.js.map +1 -1
  19. package/dist/transform/field.js +24 -1
  20. package/dist/transform/field.js.map +1 -1
  21. package/dist/transform/operation.js +131 -47
  22. package/dist/transform/operation.js.map +1 -1
  23. package/dist/transform.d.ts +1 -8
  24. package/dist/transform.js +121 -95
  25. package/dist/transform.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/types.d.ts +9 -0
  28. package/dist/types.js +3 -2
  29. package/dist/types.js.map +1 -1
  30. package/dist/utility.d.ts +7 -1
  31. package/dist/utility.js +85 -32
  32. package/dist/utility.js.map +1 -1
  33. package/model/apidef.jsonic +1 -0
  34. package/package.json +8 -9
  35. package/src/apidef.ts +23 -33
  36. package/src/builder/flow/flowHeuristic01.ts +44 -9
  37. package/src/builder/flow.ts +4 -3
  38. package/src/guide/heuristic01.ts +281 -124
  39. package/src/guide.ts +49 -14
  40. package/src/parse.ts +106 -4
  41. package/src/transform/clean.ts +28 -0
  42. package/src/transform/entity.ts +26 -3
  43. package/src/transform/field.ts +27 -1
  44. package/src/transform/operation.ts +203 -64
  45. package/src/transform.ts +29 -23
  46. package/src/types.ts +3 -2
  47. package/src/utility.ts +113 -1
  48. package/src/builder/flow/flowHeuristic01.ts~ +0 -45
@@ -2,12 +2,34 @@
2
2
 
3
3
  import { each, snakify, names } from 'jostraca'
4
4
 
5
+ import { size } from '@voxgig/struct'
5
6
 
6
7
 
8
+ import {
9
+ depluralize,
10
+ getdlog
11
+ } from '../utility'
12
+
13
+
14
+ type EntityDesc = {
15
+ name: string
16
+ origname: string
17
+ plural: string
18
+ path: Record<string, EntityPathDesc>
19
+ alias: Record<string, string>
20
+ }
21
+
22
+
23
+ type EntityPathDesc = {
24
+ op: Record<string, any>
25
+ }
26
+
27
+ // Log non-fatal wierdness.
28
+ const dlog = getdlog('apidef', __filename)
29
+
7
30
  async function heuristic01(ctx: any): Promise<Record<string, any>> {
8
31
  let guide = ctx.model.main.api.guide
9
32
 
10
-
11
33
  const entityDescs = resolveEntityDescs(ctx)
12
34
 
13
35
  // console.log('entityDescs')
@@ -18,10 +40,16 @@ async function heuristic01(ctx: any): Promise<Record<string, any>> {
18
40
  entity: entityDescs,
19
41
  }
20
42
 
43
+ // console.log('GUIDE')
44
+ // console.dir(guide, { depth: null })
45
+
21
46
  return guide
22
47
  }
23
48
 
24
49
 
50
+
51
+
52
+
25
53
  const METHOD_IDOP: Record<string, string> = {
26
54
  get: 'load',
27
55
  post: 'create',
@@ -35,191 +63,320 @@ function resolveEntityDescs(ctx: any) {
35
63
  const entityDescs: Record<string, any> = {}
36
64
  const paths = ctx.def.paths
37
65
 
38
- each(paths, (pathdef, pathname) => {
39
- // look for rightmmost /entname/{entid}
40
- const m = pathname.match(/\/([a-zA-Z0-1_-]+)\/\{([a-zA-Z0-1_-]+)\}$/)
66
+ // Analyze paths ending in .../foo/{foo}
67
+ each(paths, (pathDef: any, pathStr: string) => {
68
+
69
+ // Look for rightmmost /entname/{entid}.
70
+ let m = pathStr.match(/\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/)
71
+ // const m = pathStr.match(/\/([a-zA-Z0-1_-]+)\/\{([a-zA-Z0-1_-]+)\}$/)
41
72
  if (m) {
42
- let origentname = snakify(m[1])
43
- let entname = depluralize(origentname)
73
+ // const entdesc = resolveEntity(entityDescs, pathStr, m[1], m[2])
44
74
 
45
- let entdesc = (entityDescs[entname] = entityDescs[entname] || { name: entname })
46
- entdesc.plural = origentname
75
+ each(pathDef, (methodDef: any, methodStr: string) => {
76
+ methodStr = methodStr.toLowerCase()
47
77
 
48
- names(entdesc, entname)
49
78
 
50
- entdesc.alias = entdesc.alias || {}
79
+ if (!METHOD_IDOP[methodStr]) {
80
+ return
81
+ }
51
82
 
52
- if ('id' != m[2]) {
53
- entdesc.alias.id = m[2]
54
- entdesc.alias[m[2]] = 'id'
55
- }
83
+ const entdesc = resolveEntity(entityDescs, pathDef, pathStr, methodDef, methodStr)
56
84
 
57
- entdesc.path = (entdesc.path || {})
85
+ if (null == entdesc) {
86
+ console.log(
87
+ 'WARNING: unable to resolve entity for method ' + methodStr +
88
+ ' path ' + pathStr)
89
+ return
90
+ }
58
91
 
59
- const op: Record<string, any> = {}
60
- entdesc.path[pathname] = { op }
92
+ // if (pathStr.includes('courses')) {
93
+ // console.log('ENTRES', pathStr, methodStr)
94
+ // console.dir(ent2, { depth: null })
95
+ // }
96
+
97
+ let opname = resolveOpName(methodStr, methodDef, pathStr, entdesc)
98
+
99
+ if (null == opname) {
100
+ console.log(
101
+ 'WARNING: unable to resolve operation for method ' + methodStr +
102
+ ' path ' + pathStr)
103
+ return
104
+ }
61
105
 
62
- each(pathdef, (mdef, method) => {
63
- const opname = METHOD_IDOP[method]
64
- if (null == opname) return;
65
106
 
66
107
  const transform: Record<string, any> = {
67
108
  // reqform: '`reqdata`',
68
109
  // resform: '`body`',
69
110
  }
70
111
 
71
- const resokdef = mdef.responses[200] || mdef.responses[201]
112
+ const resokdef = methodDef.responses[200] || methodDef.responses[201]
72
113
  const resbody = resokdef?.content?.['application/json']?.schema
73
114
  if (resbody) {
74
- if (resbody[origentname]) {
75
- // TODO: use quotes when @voxgig/struct updated to support them
76
- // transform.resform = '`body."'+origentname+'"`'
77
- transform.resform = '`body.' + origentname + '`'
115
+ if (resbody[entdesc.origname]) {
116
+ transform.resform = '`body.' + entdesc.origname + '`'
78
117
  }
79
- else if (resbody[entname]) {
80
- transform.resform = '`body.' + entname + '`'
118
+ else if (resbody[entdesc.name]) {
119
+ transform.resform = '`body.' + entdesc.name + '`'
81
120
  }
82
121
  }
83
122
 
84
- const reqdef = mdef.requestBody?.content?.['application/json']?.schema?.properties
123
+ const reqdef = methodDef.requestBody?.content?.['application/json']?.schema?.properties
85
124
  if (reqdef) {
86
- if (reqdef[origentname]) {
87
- transform.reqform = { [origentname]: '`reqdata`' }
125
+ if (reqdef[entdesc.origname]) {
126
+ transform.reqform = { [entdesc.origname]: '`reqdata`' }
88
127
  }
89
- else if (reqdef[entname]) {
90
- transform.reqform = { [entname]: '`reqdata`' }
128
+ else if (reqdef[entdesc.origname]) {
129
+ transform.reqform = { [entdesc.origname]: '`reqdata`' }
91
130
  }
92
131
 
93
132
  }
94
133
 
134
+ const op = entdesc.path[pathStr].op
135
+
95
136
  op[opname] = {
96
137
  // TODO: in actual guide, remove "standard" method ops since redundant
97
- method,
138
+ method: methodStr,
98
139
  }
99
140
 
100
141
  if (0 < Object.entries(transform).length) {
101
142
  op[opname].transform = transform
102
143
  }
144
+
145
+ if ('/v2/users/{user_id}/enrollment' === pathStr) {
146
+ console.log('ENT')
147
+ console.dir(entdesc, { depth: null })
148
+ }
103
149
  })
104
150
  }
105
151
  })
106
152
 
153
+ // console.log('USER')
154
+ // console.dir(entityDescs.user, { depth: null })
107
155
 
108
- each(paths, (pathdef, pathname) => {
109
- // look for rightmmost /entname/{entid}
110
- const m = pathname.match(/\/([a-zA-Z0-1_-]+)$/)
111
- if (m) {
112
- let origentname = snakify(m[1])
113
- let entname = depluralize(origentname)
114
-
115
- let entdesc = entityDescs[entname]
116
-
117
- if (entdesc) {
118
- entdesc.path = (entdesc.path || {})
119
-
120
- if (pathdef.get) {
121
- const op: Record<string, any> = { list: { method: 'get' } }
122
- entdesc.path[pathname] = { op }
123
-
124
- const transform: Record<string, any> = {}
125
- const mdef = pathdef.get
126
- const resokdef = mdef.responses[200] || mdef.responses[201]
127
- const resbody = resokdef?.content?.['application/json']?.schema
128
- if (resbody) {
129
- if (resbody[origentname]) {
130
- // TODO: use quotes when @voxgig/struct updated to support them
131
- // transform.resform = '`body."'+origentname+'"`'
132
- transform.resform = '`body.' + origentname + '`'
133
- }
134
- else if (resbody[entname]) {
135
- transform.resform = '`body.' + entname + '`'
136
- }
137
- }
156
+ return entityDescs
157
+ }
138
158
 
139
- if (0 < Object.entries(transform).length) {
140
- op.transform = transform
141
- }
142
- }
143
- }
159
+
160
+ function resolveEntity(
161
+ entityDescs: Record<string, EntityDesc>,
162
+ pathDef: Record<string, any>,
163
+ pathStr: string,
164
+ methodDef: Record<string, any>,
165
+ methodStr: string,
166
+ ): EntityDesc | undefined {
167
+
168
+ let entdesc: EntityDesc
169
+ let entname: string = ''
170
+ let origentname: string = ''
171
+
172
+ const m = pathStr.match(/\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/)
173
+ if (m) {
174
+ let pathName = m[1]
175
+ origentname = snakify(pathName)
176
+
177
+ // Check schema
178
+ const compname = resolveComponentName(methodDef, methodStr)
179
+ if (compname) {
180
+ origentname = snakify(compname)
144
181
  }
145
- })
146
182
 
147
- return entityDescs
148
- }
183
+ entname = depluralize(origentname)
149
184
 
185
+ entdesc = (entityDescs[entname] = entityDescs[entname] || {
186
+ name: entname,
187
+ id: Math.random(),
188
+ alias: {}
189
+ })
150
190
 
151
- function depluralize(word: string): string {
152
- if (!word || word.length === 0) {
153
- return word
191
+ let pathParam = m[3]
192
+ if (null != pathParam) {
193
+ const pathParamCanon = snakify(pathParam)
194
+ if ('id' != pathParamCanon) {
195
+ entdesc.alias.id = pathParamCanon
196
+ entdesc.alias[pathParamCanon] = 'id'
197
+ }
198
+ }
154
199
  }
155
200
 
156
- // Common irregular plurals
157
- const irregulars: Record<string, string> = {
158
- 'children': 'child',
159
- 'men': 'man',
160
- 'women': 'woman',
161
- 'teeth': 'tooth',
162
- 'feet': 'foot',
163
- 'geese': 'goose',
164
- 'mice': 'mouse',
165
- 'people': 'person',
166
- 'data': 'datum',
167
- 'criteria': 'criterion',
168
- 'phenomena': 'phenomenon',
169
- 'indices': 'index',
170
- 'matrices': 'matrix',
171
- 'vertices': 'vertex',
172
- 'analyses': 'analysis',
173
- 'axes': 'axis',
174
- 'crises': 'crisis',
175
- 'diagnoses': 'diagnosis',
176
- 'oases': 'oasis',
177
- 'theses': 'thesis',
178
- 'appendices': 'appendix'
201
+ // Can't figure out the entity
202
+ else {
203
+ console.log('NO ENTTIY', pathStr)
204
+ return
179
205
  }
180
206
 
181
- if (irregulars[word]) {
182
- return irregulars[word]
183
- }
184
207
 
185
- // Rules for regular plurals (applied in order)
208
+ // entdesc.plural = origentname
209
+ entdesc.origname = origentname
186
210
 
187
- // -ies -> -y (cities -> city)
188
- if (word.endsWith('ies') && word.length > 3) {
189
- return word.slice(0, -3) + 'y'
190
- }
211
+ names(entdesc, entname)
212
+
213
+ entdesc.alias = entdesc.alias || {}
214
+
215
+ entdesc.path = (entdesc.path || {})
216
+ entdesc.path[pathStr] = entdesc.path[pathStr] || {}
217
+ entdesc.path[pathStr].op = entdesc.path[pathStr].op || {}
191
218
 
192
- // -ves -> -f or -fe (wolves -> wolf, knives -> knife)
193
- if (word.endsWith('ves')) {
194
- const stem = word.slice(0, -3)
195
- // Check if it should be -fe (like knife, wife, life)
196
- if (['kni', 'wi', 'li'].includes(stem)) {
197
- return stem + 'fe'
219
+ return entdesc
220
+ }
221
+
222
+
223
+ const REQKIND: any = {
224
+ get: 'res',
225
+ post: 'req',
226
+ put: 'req',
227
+ patch: 'req',
228
+ }
229
+
230
+
231
+ function resolveComponentName(
232
+ methodDef: Record<string, any>,
233
+ methodStr: string,
234
+ ): string | undefined {
235
+ const kind = REQKIND[methodStr]
236
+ let compname: string | undefined = undefined
237
+
238
+ const responses = methodDef.responses
239
+ const schemalist =
240
+ [
241
+ methodDef.requestBody?.content,
242
+ responses?.['201'],
243
+ responses?.['200'],
244
+ ]
245
+ .filter(cmp => null != cmp)
246
+ .map(content => content['application/json']?.schema)
247
+ .filter(schema => null != schema)
248
+ .filter(schema => null != schema['x-ref'])
249
+ .map(schema => {
250
+ let xrefm = schema['x-ref'].match(/\/components\/schemas\/(.+)$/)
251
+ if (xrefm) {
252
+ schema['x-ref-cmp'] = xrefm[1]
253
+ }
254
+ return schema
255
+ })
256
+ .filter(schema => null != schema['x-ref-cmp'])
257
+
258
+ let schema = undefined
259
+ let splen = -1
260
+
261
+ for (let sI = 0; sI < schemalist.length; sI++) {
262
+ let nextschema = schemalist[sI]
263
+ let nsplen = nextschema.properties?.length || -1
264
+
265
+ // console.log('QQQ', splen, nsplen, schema?.['x-ref-cmp'], nextschema?.['x-ref-cmp'])
266
+
267
+ if (
268
+ // More properties probably means it is the full entity.
269
+ splen < nsplen ||
270
+
271
+ // Shorter name probably means it is the full entity (no suffix/prefix).
272
+ (schema && splen === nsplen && nextschema['x-ref-cmp'].length < schema['x-ref-cmp'].length)
273
+
274
+ ) {
275
+ schema = nextschema
276
+ splen = nsplen
198
277
  }
199
- return stem + 'f'
200
278
  }
201
279
 
202
- // -oes -> -o (potatoes -> potato)
203
- if (word.endsWith('oes')) {
204
- return word.slice(0, -2)
280
+ if (schema) {
281
+ let xref = schema['x-ref']
282
+ // console.log('RCN-XREF', methodStr, 'xref-0', xref)
283
+
284
+ if (null == xref) {
285
+ const properties = schema.properties || {}
286
+ each(properties, (prop) => {
287
+ if (null == xref) {
288
+ if (prop.type === 'array') {
289
+ xref = prop.items?.['x-ref']
290
+ // console.log('RCN', methodStr, 'xref-1', xref)
291
+ }
292
+ }
293
+ })
294
+ }
295
+
296
+ if (null != xref && 'string' === typeof xref) {
297
+ let xrefm = xref.match(/\/components\/schemas\/(.+)$/)
298
+ if (xrefm) {
299
+ compname = xrefm[1]
300
+ }
301
+ }
205
302
  }
206
303
 
207
- // -ses, -xes, -zes, -shes, -ches -> remove -es (boxes -> box)
208
- if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes') ||
209
- word.endsWith('shes') || word.endsWith('ches')) {
210
- return word.slice(0, -2)
304
+ return compname
305
+ }
306
+
307
+
308
+ function resolveOpName(methodStr: string, methodDef: any, pathStr: string, entdesc: EntityDesc)
309
+ : string | undefined {
310
+
311
+ let opname = METHOD_IDOP[methodStr]
312
+ if (null == opname) return;
313
+
314
+ if ('load' === opname) {
315
+ const islist = isListResponse(methodDef, pathStr, entdesc)
316
+ opname = islist ? 'list' : opname
317
+
318
+ console.log('ISLIST', entdesc.name, methodStr, opname, pathStr)
211
319
  }
212
320
 
213
- // -s -> remove -s (cats -> cat)
214
- if (word.endsWith('s') && !word.endsWith('ss')) {
215
- return word.slice(0, -1)
321
+ return opname
322
+ }
323
+
324
+
325
+ function isListResponse(
326
+ methodDef: Record<string, any>,
327
+ pathStr: string,
328
+ entdesc: EntityDesc
329
+ ): boolean {
330
+ const responses = methodDef.responses
331
+ const resdef = responses?.['201'] || responses?.['200']
332
+ const content = resdef?.content
333
+
334
+ let islist = false
335
+
336
+ if (null != content) {
337
+ const schema = content['application/json']?.schema
338
+ if (schema) {
339
+ if (schema.type === 'array') {
340
+ islist = true
341
+ }
342
+
343
+ if (!islist) {
344
+ const properties = schema.properties || {}
345
+ each(properties, (prop) => {
346
+ if (prop.type === 'array') {
347
+
348
+ if (
349
+ 1 === size(properties) ||
350
+ prop.key$ === entdesc.name ||
351
+ prop.key$ === entdesc.origname ||
352
+ listedEntity(prop) === entdesc.name
353
+ ) {
354
+ islist = true
355
+ }
356
+
357
+ if ('/v2/users' === pathStr) {
358
+ console.log('islistresponse', islist, pathStr, entdesc.name, listedEntity(prop), properties)
359
+ }
360
+ }
361
+ })
362
+ }
363
+ }
216
364
  }
217
365
 
218
- // If none of the rules apply, return as is
219
- return word
366
+ return islist
220
367
  }
221
368
 
222
369
 
370
+ function listedEntity(prop: any) {
371
+ const xref = prop?.items?.['x-ref']
372
+ const m = 'string' === typeof xref && xref.match(/^#\/components\/schemas\/(.+)$/)
373
+ if (m) {
374
+ return depluralize(snakify(m[1]))
375
+ }
376
+ }
377
+
378
+
379
+
223
380
  export {
224
381
  heuristic01
225
382
  }
package/src/guide.ts CHANGED
@@ -3,43 +3,68 @@ import Path from 'node:path'
3
3
 
4
4
  import { File, Content, each } from 'jostraca'
5
5
 
6
+ import { merge } from '@voxgig/struct'
7
+
6
8
 
7
9
  import { heuristic01 } from './guide/heuristic01'
8
10
 
11
+ import {
12
+ getdlog,
13
+ } from './utility'
9
14
 
10
- async function resolveGuide(ctx: any) {
11
- // console.log('GUIDE CTX', ctx)
12
15
 
13
- let guide: Record<string, any> = ctx.model.main.api.guide
16
+ // Log non-fatal wierdness.
17
+ const dlog = getdlog('apidef', __filename)
18
+
19
+ async function resolveGuide(ctx: any) {
20
+ let baseguide: Record<string, any> = {}
21
+ let override: Record<string, any> = ctx.model.main.api.guide
14
22
 
15
23
  if ('heuristic01' === ctx.opts.strategy) {
16
- guide = await heuristic01(ctx)
24
+ baseguide = await heuristic01(ctx)
17
25
  }
18
26
  else {
19
27
  throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
20
28
  }
21
29
 
30
+ // Override generated base guide with custom hints
31
+ let guide = merge([{}, baseguide, override])
32
+
33
+ // TODO: this is a hack!!!
34
+ // Instead, update @voxgig/model, so that builders can request a reload of the entire
35
+ // model. This allows builders to modify the model for later buidlers
36
+ // during a single generation pass.
37
+
38
+
22
39
  guide = cleanGuide(guide)
23
40
 
24
- ctx.model.main.api.guide = guide
41
+
42
+ // TODO: FIX: sdk.jsonic should have final version of guide
43
+ if (ctx.model.main?.api) {
44
+ ctx.model.main.api.guide = guide
45
+ }
46
+ else {
47
+ dlog('missing', 'ctx.model.main.api')
48
+ }
25
49
 
26
50
  const guideFile =
27
51
  Path.join(ctx.opts.folder,
28
- (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'guide.jsonic')
52
+ (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'base-guide.jsonic')
29
53
 
30
54
  const guideBlocks = [
31
55
  '# Guide',
32
56
  '',
33
57
  'main: api: guide: { ',
34
- '',
58
+
35
59
  ]
36
60
 
37
61
 
38
- guideBlocks.push(...each(guide.entity, (entity, entityname) => {
39
- guideBlocks.push(`\nentity: ${entityname}: path: {`)
62
+ guideBlocks.push(...each(baseguide.entity, (entity, entityname) => {
63
+ guideBlocks.push(`
64
+ entity: ${entityname}: {`)
40
65
 
41
66
  each(entity.path, (path, pathname) => {
42
- guideBlocks.push(` '${pathname}': op: {`)
67
+ guideBlocks.push(` path: '${pathname}': op: {`)
43
68
 
44
69
  each(path.op, (op, opname) => {
45
70
  guideBlocks.push(` '${opname}': method: ${op.method}`)
@@ -59,11 +84,9 @@ async function resolveGuide(ctx: any) {
59
84
 
60
85
  const guideSrc = guideBlocks.join('\n')
61
86
 
62
- // console.log(guideSrc)
63
-
64
87
  return () => {
65
- File({ name: Path.basename(guideFile) }, () => Content(guideSrc))
66
-
88
+ // Save base guide for reference
89
+ File({ name: '../def/' + Path.basename(guideFile) }, () => Content(guideSrc))
67
90
  }
68
91
  }
69
92
 
@@ -74,7 +97,19 @@ function cleanGuide(guide: Record<string, any>): Record<string, any> {
74
97
  entity: {}
75
98
  }
76
99
 
100
+ const exclude_entity = guide.exclude?.entity?.split(',') || []
101
+ const include_entity = guide.include?.entity?.split(',') || []
102
+
77
103
  each(guide.entity, (entity: any, name: string) => {
104
+ if (exclude_entity.includes(name)) {
105
+ return
106
+ }
107
+ if (exclude_entity.includes('*')) {
108
+ if (!include_entity.includes(name)) {
109
+ return
110
+ }
111
+ }
112
+
78
113
  let ent: any = clean.entity[name] = clean.entity[name] = { name, path: {} }
79
114
 
80
115
  each(entity.path, (path: any, pathname: string) => {