@voxgig/apidef 2.4.0 → 3.0.2

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 (96) hide show
  1. package/dist/apidef.d.ts +5 -1
  2. package/dist/apidef.js +197 -112
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.d.ts +3 -0
  5. package/dist/builder/entity/{apiEntity.js → entity.js} +12 -9
  6. package/dist/builder/entity/entity.js.map +1 -0
  7. package/dist/builder/entity/info.d.ts +3 -0
  8. package/dist/builder/entity/info.js +22 -0
  9. package/dist/builder/entity/info.js.map +1 -0
  10. package/dist/builder/entity.js +7 -21
  11. package/dist/builder/entity.js.map +1 -1
  12. package/dist/builder/flow/flowHeuristic01.js +21 -11
  13. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  14. package/dist/builder/flow.d.ts +2 -1
  15. package/dist/builder/flow.js +29 -4
  16. package/dist/builder/flow.js.map +1 -1
  17. package/dist/def.d.ts +62 -0
  18. package/dist/def.js +4 -0
  19. package/dist/def.js.map +1 -0
  20. package/dist/desc.d.ts +89 -0
  21. package/dist/desc.js +4 -0
  22. package/dist/desc.js.map +1 -0
  23. package/dist/guide/guide.d.ts +2 -1
  24. package/dist/guide/guide.js +161 -30
  25. package/dist/guide/guide.js.map +1 -1
  26. package/dist/guide/heuristic01.d.ts +2 -1
  27. package/dist/guide/heuristic01.js +1120 -234
  28. package/dist/guide/heuristic01.js.map +1 -1
  29. package/dist/model.d.ts +55 -0
  30. package/dist/model.js +4 -0
  31. package/dist/model.js.map +1 -0
  32. package/dist/parse.d.ts +1 -2
  33. package/dist/parse.js +8 -47
  34. package/dist/parse.js.map +1 -1
  35. package/dist/transform/args.d.ts +3 -0
  36. package/dist/transform/args.js +58 -0
  37. package/dist/transform/args.js.map +1 -0
  38. package/dist/transform/clean.js +27 -3
  39. package/dist/transform/clean.js.map +1 -1
  40. package/dist/transform/entity.d.ts +11 -3
  41. package/dist/transform/entity.js +57 -41
  42. package/dist/transform/entity.js.map +1 -1
  43. package/dist/transform/field.d.ts +3 -3
  44. package/dist/transform/field.js +90 -65
  45. package/dist/transform/field.js.map +1 -1
  46. package/dist/transform/operation.d.ts +1 -1
  47. package/dist/transform/operation.js +94 -296
  48. package/dist/transform/operation.js.map +1 -1
  49. package/dist/transform/select.d.ts +3 -0
  50. package/dist/transform/select.js +44 -0
  51. package/dist/transform/select.js.map +1 -0
  52. package/dist/transform/top.d.ts +9 -0
  53. package/dist/transform/top.js +11 -2
  54. package/dist/transform/top.js.map +1 -1
  55. package/dist/transform.js +4 -0
  56. package/dist/transform.js.map +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/dist/types.d.ts +112 -19
  59. package/dist/types.js +4 -2
  60. package/dist/types.js.map +1 -1
  61. package/dist/utility.d.ts +30 -2
  62. package/dist/utility.js +381 -6
  63. package/dist/utility.js.map +1 -1
  64. package/model/apidef.jsonic +75 -1
  65. package/model/guide.jsonic +14 -44
  66. package/package.json +19 -14
  67. package/src/apidef.ts +264 -121
  68. package/src/builder/entity/{apiEntity.ts → entity.ts} +18 -11
  69. package/src/builder/entity/info.ts +53 -0
  70. package/src/builder/entity.ts +9 -35
  71. package/src/builder/flow/flowHeuristic01.ts +46 -12
  72. package/src/builder/flow.ts +39 -5
  73. package/src/def.ts +91 -0
  74. package/src/desc.ts +143 -0
  75. package/src/guide/guide.ts +207 -134
  76. package/src/guide/heuristic01.ts +1651 -272
  77. package/src/model.ts +98 -0
  78. package/src/parse.ts +5 -61
  79. package/src/schematron.ts.off +317 -0
  80. package/src/transform/args.ts +102 -0
  81. package/src/transform/clean.ts +43 -8
  82. package/src/transform/entity.ts +100 -51
  83. package/src/transform/field.ts +150 -71
  84. package/src/transform/operation.ts +118 -414
  85. package/src/transform/select.ts +90 -0
  86. package/src/transform/top.ts +76 -3
  87. package/src/transform.ts +4 -0
  88. package/src/types.ts +185 -5
  89. package/src/utility.ts +481 -9
  90. package/dist/builder/entity/apiEntity.d.ts +0 -3
  91. package/dist/builder/entity/apiEntity.js.map +0 -1
  92. package/dist/builder/entity/def.d.ts +0 -3
  93. package/dist/builder/entity/def.js +0 -19
  94. package/dist/builder/entity/def.js.map +0 -1
  95. package/src/builder/entity/def.ts +0 -44
  96. package/src/guide.ts.off +0 -136
@@ -1,57 +1,188 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.heuristic01 = heuristic01;
4
+ const ordu_1 = require("ordu");
4
5
  const jostraca_1 = require("jostraca");
5
6
  const struct_1 = require("@voxgig/struct");
6
7
  const utility_1 = require("../utility");
7
- // Log non-fatal wierdness.
8
+ // Log non - fatal wierdness.
8
9
  const dlog = (0, utility_1.getdlog)('apidef', __filename);
10
+ // Schema components that occur less than this rate(over total method count) qualify
11
+ // as unique entities, not shared schemas
12
+ const IS_ENTCMP_METHOD_RATE = 0.21;
13
+ const IS_ENTCMP_PATH_RATE = 0.41;
14
+ const METHOD_IDOP = {
15
+ GET: 'load',
16
+ POST: 'create',
17
+ PUT: 'update',
18
+ DELETE: 'remove',
19
+ PATCH: 'patch',
20
+ HEAD: 'head',
21
+ OPTIONS: 'OPTIONS',
22
+ };
23
+ const METHOD_CONSIDER_ORDER = {
24
+ 'GET': 100,
25
+ 'POST': 200,
26
+ 'PUT': 300,
27
+ 'PATCH': 400,
28
+ 'DELETE': 500,
29
+ 'HEAD': 600,
30
+ 'OPTIONS': 700,
31
+ };
9
32
  async function heuristic01(ctx) {
10
- let guide = ctx.model.main.api.guide;
11
- const metrics = measure(ctx);
12
- // console.dir(metrics, { depth: null })
13
- const entityDescs = resolveEntityDescs(ctx);
14
- guide = {
15
- control: guide.control,
16
- entity: entityDescs,
17
- };
33
+ const analysis = new ordu_1.Ordu({ select: { sort: true } }).add([
34
+ Prepare,
35
+ {
36
+ select: 'def.paths', apply: [
37
+ MeasurePath,
38
+ { select: '', apply: MeasureMethod },
39
+ PreparePath
40
+ ]
41
+ },
42
+ { select: selectCmpXrefs, apply: MeasureRef },
43
+ {
44
+ select: selectAllMethods, apply: [
45
+ ResolveEntityComponent,
46
+ ResolveEntityName,
47
+ RenameParams,
48
+ FindActions,
49
+ ResolveOperation,
50
+ ResolveTransform,
51
+ // ShowNode,
52
+ ]
53
+ },
54
+ { select: 'work.entmap', apply: BuildEntity }
55
+ ]);
56
+ const result = analysis.execSync(ctx, {});
57
+ if (result.err) {
58
+ throw result.err;
59
+ }
60
+ const guide = result.data.guide;
61
+ // console.log('WORK', result.data.work)
62
+ // console.log('GUIDE')
63
+ // console.dir(guide, { depth: null })
64
+ // TODO: move to Ordu
65
+ // warnOnError('reviewEntityDescs', ctx.warn, () => reviewEntityDescs(ctx, result))
18
66
  return guide;
19
67
  }
20
- function measure(ctx) {
21
- const metrics = {
22
- count: {
23
- schema: {}
24
- }
68
+ function ShowNode(spec) {
69
+ console.log('NODE', spec.node.key, spec.node.val);
70
+ }
71
+ function Prepare(spec) {
72
+ const guide = {
73
+ control: {},
74
+ entity: {},
75
+ metrics: {
76
+ count: {
77
+ path: 0,
78
+ method: 0,
79
+ tag: 0,
80
+ cmp: 0,
81
+ entity: 0,
82
+ origcmprefs: {},
83
+ },
84
+ found: {
85
+ tag: {},
86
+ cmp: {},
87
+ }
88
+ },
25
89
  };
26
- let xrefs = (0, utility_1.find)(ctx.def, 'x-ref');
27
- // console.log('XREFS', xrefs)
28
- let schemas = xrefs.filter(xref => xref.val.includes('schema'));
29
- schemas.map(schema => {
30
- let m = schema.val.match(/\/components\/schemas\/(.+)$/);
31
- if (m) {
32
- const name = m[1];
33
- metrics.count.schema[name] = 1 + (metrics.count.schema[name] || 0);
90
+ Object.assign(spec.data, {
91
+ def: spec.ctx.def,
92
+ guide,
93
+ work: {
94
+ pathmap: {},
95
+ entmap: {},
96
+ entity: {
97
+ count: {
98
+ seen: 0,
99
+ unresolved: 0,
100
+ }
101
+ },
34
102
  }
35
103
  });
36
- return metrics;
37
104
  }
38
- const METHOD_IDOP = {
39
- get: 'load',
40
- post: 'create',
41
- put: 'update',
42
- patch: 'update',
43
- delete: 'remove',
44
- };
45
- function resolveEntityDescs(ctx) {
46
- const entityDescs = {};
47
- const paths = ctx.def.paths;
48
- const caught = (0, utility_1.capture)(ctx.def, {
49
- paths: ['`$SELECT`', /\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/,
50
- ['`$SELECT`', /get|post|put|patch|delete/i,
105
+ // Expects to run over paths
106
+ function MeasurePath(spec) {
107
+ const guide = spec.data.guide;
108
+ const metrics = guide.metrics;
109
+ // const pathstr = spec.node.key
110
+ const pathdef = spec.node.val;
111
+ metrics.count.path++;
112
+ metrics.count.method += ((pathdef.get ? 1 : 0) +
113
+ (pathdef.post ? 1 : 0) +
114
+ (pathdef.put ? 1 : 0) +
115
+ (pathdef.patch ? 1 : 0) +
116
+ (pathdef.delete ? 1 : 0) +
117
+ (pathdef.head ? 1 : 0) +
118
+ (pathdef.options ? 1 : 0));
119
+ }
120
+ // Expects to run over paths.<method>
121
+ function MeasureMethod(spec) {
122
+ const guide = spec.data.guide;
123
+ const metrics = guide.metrics;
124
+ // const methodstr = spec.node.key
125
+ const methoddef = spec.node.val;
126
+ const pathtags = methoddef.tags;
127
+ if (Array.isArray(pathtags)) {
128
+ for (let tag of pathtags) {
129
+ if ('string' === typeof tag && 0 < tag.length) {
130
+ if (!metrics.found.tag[tag]) {
131
+ metrics.count.tag++;
132
+ metrics.found.tag[tag] = {
133
+ name: tag,
134
+ canon: (0, utility_1.canonize)(tag),
135
+ };
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ function PreparePath(spec) {
142
+ const work = spec.data.work;
143
+ const pathstr = spec.node.key;
144
+ const pathdef = spec.node.val;
145
+ const pathdesc = {
146
+ path: pathstr,
147
+ def: pathdef,
148
+ parts: pathstr.split('/').filter((p) => '' != p),
149
+ op: {}
150
+ };
151
+ work.pathmap[pathstr] = pathdesc;
152
+ }
153
+ function selectCmpXrefs(_source, spec) {
154
+ const out = (0, utility_1.find)(spec.ctx.def, 'x-ref')
155
+ .filter(xref => xref.val.match(/\/(components\/schemas|definitions)\//));
156
+ // console.log('selectCmpXrefs', out)
157
+ return out;
158
+ }
159
+ function MeasureRef(spec) {
160
+ const guide = spec.data.guide;
161
+ const metrics = guide.metrics;
162
+ let m = spec.node.val.val.match(/\/(components\/schemas|definitions)\/(.+)$/);
163
+ if (m) {
164
+ const name = (0, utility_1.canonize)(m[2]);
165
+ if (null == metrics.count.origcmprefs[name]) {
166
+ metrics.count.cmp++;
167
+ metrics.count.origcmprefs[name] = 0;
168
+ }
169
+ metrics.count.origcmprefs[name]++;
170
+ if (null == metrics.found.cmp[name]) {
171
+ metrics.found.cmp[name] = { orig: m[2] };
172
+ }
173
+ }
174
+ }
175
+ function selectAllMethods(_source, spec) {
176
+ const ctx = spec.ctx;
177
+ // const paths = ctx.def.paths
178
+ let caught = (0, utility_1.capture)(ctx.def, {
179
+ paths: ['`$SELECT`', /.*/,
180
+ ['`$SELECT`', /^get|post|put|patch|delete$/i,
51
181
  ['`$APPEND`', 'methods', {
52
182
  path: '`select$=key.paths`',
53
- method: { '`$LOWER`': '`$KEY`' },
183
+ method: { '`$UPPER`': '`$KEY`' },
54
184
  summary: '`.summary`',
185
+ tags: '`.tags`',
55
186
  parameters: '`.parameters`',
56
187
  responses: '`.responses`',
57
188
  requestBody: '`.requestBody`'
@@ -59,239 +190,994 @@ function resolveEntityDescs(ctx) {
59
190
  ]
60
191
  ]
61
192
  });
62
- (0, jostraca_1.each)(caught.methods, (pmdef) => {
63
- let methodDef = pmdef;
64
- let pathStr = pmdef.path;
65
- let methodStr = pmdef.method;
66
- // methodStr = methodStr.toLowerCase()
67
- let why_op = [];
68
- if (!METHOD_IDOP[methodStr]) {
69
- return;
70
- }
71
- const why_ent = [];
72
- const entdesc = resolveEntity(entityDescs, pathStr, methodDef, methodStr, why_ent);
73
- if (null == entdesc) {
74
- console.log('WARNING: unable to resolve entity for method ' + methodStr +
75
- ' path ' + pathStr);
76
- return;
77
- }
78
- entdesc.path[pathStr].why_ent = why_ent;
79
- // if (pathStr.includes('courses')) {
80
- // console.log('ENTRES', pathStr, methodStr)
81
- // console.dir(ent2, { depth: null })
82
- // }
83
- let opname = resolveOpName(methodStr, methodDef, pathStr, entdesc, why_op);
84
- if (null == opname) {
85
- console.log('WARNING: unable to resolve operation for method ' + methodStr +
86
- ' path ' + pathStr);
87
- return;
88
- }
89
- const transform = {
90
- // reqform: '`reqdata`',
91
- // resform: '`body`',
92
- };
93
- const resokdef = methodDef.responses?.[200] || methodDef.responses?.[201];
94
- const resbody = resokdef?.content?.['application/json']?.schema;
95
- if (resbody) {
96
- if (resbody[entdesc.origname]) {
97
- transform.resform = '`body.' + entdesc.origname + '`';
98
- }
99
- else if (resbody[entdesc.name]) {
100
- transform.resform = '`body.' + entdesc.name + '`';
101
- }
193
+ // TODO: capture should return these empty objects
194
+ caught = caught ?? {};
195
+ caught.methods = caught.methods ?? [];
196
+ caught.methods.sort((a, b) => {
197
+ if (a.path < b.path) {
198
+ return -1;
102
199
  }
103
- const reqdef = methodDef.requestBody?.content?.['application/json']?.schema?.properties;
104
- if (reqdef) {
105
- if (reqdef[entdesc.origname]) {
106
- transform.reqform = { [entdesc.origname]: '`reqdata`' };
107
- }
108
- else if (reqdef[entdesc.origname]) {
109
- transform.reqform = { [entdesc.origname]: '`reqdata`' };
110
- }
200
+ else if (a.path > b.path) {
201
+ return 1;
111
202
  }
112
- const op = entdesc.path[pathStr].op;
113
- op[opname] = {
114
- // TODO: in actual guide, remove "standard" method ops since redundant
115
- method: methodStr,
116
- why_op: why_op.join(';')
117
- };
118
- if (0 < Object.entries(transform).length) {
119
- op[opname].transform = transform;
203
+ else if (METHOD_CONSIDER_ORDER[a.method] < METHOD_CONSIDER_ORDER[b.method]) {
204
+ return -1;
120
205
  }
121
- // if ('/v2/users/{user_id}/enrollment' === pathStr) {
122
- // console.log('ENT')
123
- // console.dir(entdesc, { depth: null })
124
- // }
125
- // })
126
- // }
127
- });
128
- // console.log('USER')
129
- // console.dir(entityDescs.user, { depth: null })
130
- return entityDescs;
131
- }
132
- function resolveEntity(entityDescs,
133
- // pathDef: Record<string, any>,
134
- pathStr, methodDef, methodStr, why_ent) {
135
- let entdesc;
136
- let entname = '';
137
- let origentname = '';
138
- const why_name = [];
139
- const m = pathStr.match(/\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/);
140
- if (m) {
141
- let pathName = m[1];
142
- let pathParam = m[3];
143
- origentname = (0, jostraca_1.snakify)(pathName);
144
- entname = (0, utility_1.depluralize)(origentname);
145
- // Check schema
146
- const compname = resolveComponentName(entname, methodDef, methodStr, pathStr, why_name);
147
- if (compname) {
148
- origentname = (0, jostraca_1.snakify)(compname);
149
- entname = (0, utility_1.depluralize)(origentname);
150
- why_ent.push('cmp:' + entname);
206
+ else if (METHOD_CONSIDER_ORDER[a.method] > METHOD_CONSIDER_ORDER[b.method]) {
207
+ return 1;
151
208
  }
152
209
  else {
153
- why_ent.push('path:' + m[1]);
154
- why_name.push('path:' + m[1]);
210
+ return 0;
211
+ }
212
+ });
213
+ // console.log(caught.methods.map((n: any) => n.path + ' ' + n.method))
214
+ return caught.methods || [];
215
+ }
216
+ function ResolveEntityComponent(spec) {
217
+ const guide = spec.data.guide;
218
+ const metrics = guide.metrics;
219
+ const work = spec.data.work;
220
+ const methodDef = spec.node.val;
221
+ const methodName = methodDef.method;
222
+ const pathStr = methodDef.path;
223
+ const parts = work.pathmap[pathStr].parts;
224
+ let why_cmp = [];
225
+ let responses = methodDef.responses;
226
+ let origxrefs = findPotentialSchemaRefs(pathStr, methodName, responses).map(val => ({
227
+ val
228
+ }));
229
+ let cmpxrefs = origxrefs
230
+ .filter(xref => xref.val.includes('schema') || xref.val.includes('definitions'))
231
+ .map(xref => {
232
+ let m = xref.val.match(/\/components\/schemas\/(.+)$/);
233
+ if (!m) {
234
+ m = xref.val.match(/\/definitions\/(.+)$/);
235
+ }
236
+ if (m) {
237
+ const cmp = (0, utility_1.canonize)(m[1]);
238
+ xref.cmp = cmp;
239
+ xref.origcmp = m[1];
240
+ xref.origcmpref = cmp;
241
+ }
242
+ return xref;
243
+ })
244
+ .filter(xref => null != xref.cmp)
245
+ // TODO: identify non - ent schemas
246
+ .filter(xref => !xref.val.includes('Meta'));
247
+ let cleanxrefs = cmpxrefs
248
+ .map(xref => {
249
+ // Redundancy in cmp name, remove request,response suffix
250
+ // const lastPart = getelem(pathStr.split('/'), -1)
251
+ const lastPart = (0, struct_1.getelem)(parts, -1);
252
+ const lastPartLower = lastPart?.toLowerCase();
253
+ const lastPartCanon = (0, utility_1.canonize)(lastPart);
254
+ const origcmpLower = xref.origcmp?.toLowerCase();
255
+ if ('' !== lastPartCanon
256
+ && (xref.cmp === lastPartCanon + '_response'
257
+ || xref.cmp === lastPartCanon + '_request'
258
+ || origcmpLower === lastPartLower + 'response'
259
+ || origcmpLower === lastPartLower + 'request')) {
260
+ let cparts = xref.cmp.split('_');
261
+ // rec-canonize to deal with plural before removed suffix
262
+ xref.cmp = (0, utility_1.canonize)(cparts.slice(0, cparts.length - 1).join('_'));
263
+ }
264
+ return xref;
265
+ });
266
+ let goodxrefs = cleanxrefs
267
+ .filter(xref => {
268
+ if (cleanxrefs.length <= 1
269
+ || pathStr.toLowerCase().includes('/' + xref.cmp + '/')
270
+ // || entityOccursInPath(pathStr.toLowerCase(), xref.cmp)
271
+ || entityOccursInPath(parts, xref.cmp)) {
272
+ return true;
155
273
  }
156
- entdesc = (entityDescs[entname] = entityDescs[entname] || {
157
- name: entname,
158
- id: Math.random(),
159
- alias: {}
274
+ // Exclude high frequency suspicious cmps as probably meta data
275
+ const cmprefs = metrics.count.origcmprefs[xref.origcmpref] ?? 0;
276
+ const mcount = metrics.count.method;
277
+ const pcount = metrics.count.path;
278
+ const method_rate = (0 < mcount ? (cmprefs / mcount) : -1);
279
+ const path_rate = (0 < pcount ? (cmprefs / pcount) : -1);
280
+ // console.log('RCN', xref.cmp, cmprefs, mcount, method_rate, IS_ENTCMP_METHOD_RATE, method_rate < IS_ENTCMP_METHOD_RATE)
281
+ const infrequent = method_rate < IS_ENTCMP_METHOD_RATE
282
+ || path_rate < IS_ENTCMP_PATH_RATE;
283
+ if (!infrequent) {
284
+ (0, utility_1.debugpath)(pathStr, methodName, 'CMP-INFREQ', xref.val, 'method:', method_rate, IS_ENTCMP_METHOD_RATE, 'path:', path_rate, IS_ENTCMP_PATH_RATE);
285
+ }
286
+ return infrequent;
287
+ });
288
+ // .sort((a, b) => a.path.length - b.path.length)
289
+ const fcmp = goodxrefs[0];
290
+ let out = undefined;
291
+ if (null != fcmp) {
292
+ out = makeMethodEntityDesc({
293
+ ref: fcmp.val,
294
+ cmp: fcmp.cmp,
295
+ origcmp: fcmp.origcmp,
296
+ origcmpref: fcmp.origcmpref,
297
+ entname: fcmp.cmp,
160
298
  });
161
- if (null != pathParam) {
162
- const pathParamCanon = (0, jostraca_1.snakify)(pathParam);
163
- if ('id' != pathParamCanon) {
164
- entdesc.alias.id = pathParamCanon;
165
- entdesc.alias[pathParamCanon] = 'id';
299
+ }
300
+ const tags = methodDef.tags ?? [];
301
+ const goodtags = tags.filter((tag) => {
302
+ const tagdesc = metrics.found.tag[tag];
303
+ const ctag = tagdesc?.canon;
304
+ return (!!metrics.found.cmp[ctag] // tag matches a cmp
305
+ || null == fcmp // there's no cmp, so use tag
306
+ );
307
+ });
308
+ (0, utility_1.debugpath)(pathStr, methodName, 'TAGS', tags, goodtags, fcmp, methodDef, metrics.found);
309
+ const ftag = goodtags[0];
310
+ if (null != ftag) {
311
+ const tagdesc = metrics.found.tag[ftag];
312
+ const tagcmp = metrics.found.cmp[tagdesc.canon];
313
+ if (tagdesc && (tagcmp || null == fcmp)) {
314
+ if (null == out) {
315
+ out = makeMethodEntityDesc({
316
+ ref: 'tag',
317
+ cmp: tagdesc.canon,
318
+ origcmp: ftag,
319
+ why_cmp,
320
+ entname: tagdesc.canon,
321
+ });
322
+ why_cmp.push('tag=' + out.cmp);
323
+ }
324
+ else if ((pathStr.includes('/' + ftag + '/') || pathStr.includes('/' + tagdesc.canon + '/'))
325
+ && out.cmp !== tagdesc.canon) {
326
+ out = makeMethodEntityDesc({
327
+ ref: 'tag',
328
+ cmp: tagdesc.canon,
329
+ origcmp: ftag,
330
+ why_cmp,
331
+ entname: tagdesc.canon,
332
+ });
333
+ why_cmp.push('tag/path=' + out.cmp);
166
334
  }
167
335
  }
168
336
  }
169
- // Can't figure out the entity
337
+ if (null != out) {
338
+ why_cmp.push('cmp/resolve=' + out.cmp);
339
+ out.why_cmp = why_cmp;
340
+ out.cmpoccur = metrics.count.origcmprefs[out.origcmpref ?? ''] ?? 0;
341
+ out.path_rate = 0 == metrics.count.path ? -1 : (out.cmpoccur / metrics.count.path);
342
+ out.method_rate = 0 == metrics.count.method ? -1 : (out.cmpoccur / metrics.count.method);
343
+ methodDef.MethodEntity = out;
344
+ }
345
+ (0, utility_1.debugpath)(pathStr, methodName, 'CMP-NAME', out, origxrefs, cleanxrefs, goodxrefs, goodtags);
346
+ }
347
+ function ResolveEntityName(spec) {
348
+ const ctx = spec.ctx;
349
+ const data = spec.data;
350
+ const mdesc = spec.node.val;
351
+ const methodName = mdesc.method;
352
+ const pathStr = mdesc.path;
353
+ const work = spec.data.work;
354
+ const pathDesc = work.pathmap[pathStr];
355
+ const parts = pathDesc.parts;
356
+ work.entity.count.seen++;
357
+ let ment;
358
+ ment = mdesc.MethodEntity;
359
+ const why_path = [];
360
+ if (null == ment) {
361
+ why_path.push('no-desc');
362
+ mdesc.MethodEntity = makeMethodEntityDesc({});
363
+ ment = mdesc.MethodEntity;
364
+ }
365
+ why_path.push(...(ment.why_cmp ?? []));
366
+ let entname;
367
+ let pm = undefined;
368
+ if (pm = (0, utility_1.pathMatch)(parts, 't/p/t/')) {
369
+ entname = entityPathMatch_tpte(data, pm, mdesc, why_path);
370
+ }
371
+ else if (pm = (0, utility_1.pathMatch)(parts, 't/p/')) {
372
+ entname = entityPathMatch_tpe(data, pm, mdesc, why_path);
373
+ }
374
+ else if (pm = (0, utility_1.pathMatch)(parts, 'p/t/')) {
375
+ entname = entityPathMatch_pte(data, pm, mdesc, why_path);
376
+ }
377
+ else if (pm = (0, utility_1.pathMatch)(parts, 't/')) {
378
+ entname = entityPathMatch_te(data, pm, mdesc, why_path);
379
+ }
380
+ else if (pm = (0, utility_1.pathMatch)(parts, 't/p/p')) {
381
+ entname = entityPathMatch_tpp(data, pm, mdesc, why_path);
382
+ }
170
383
  else {
171
- console.log('NO ENTTIY', pathStr);
172
- return;
384
+ work.entity.count.unresolved++;
385
+ entname = 'entity' + work.entity.count.unresolved;
173
386
  }
174
- // entdesc.plural = origentname
175
- entdesc.origname = origentname;
176
- (0, jostraca_1.names)(entdesc, entname);
177
- entdesc.alias = entdesc.alias || {};
387
+ const entdesc = work.entmap[entname] = work.entmap[entname] ?? {
388
+ name: entname,
389
+ id: 'N' + ('' + Math.random()).substring(2, 10),
390
+ op: {},
391
+ why_path,
392
+ ...ment
393
+ };
178
394
  entdesc.path = (entdesc.path || {});
179
- entdesc.path[pathStr] = entdesc.path[pathStr] || {};
395
+ entdesc.path[pathStr] = entdesc.path[pathStr] || {
396
+ rename: { param: {} },
397
+ why_rename: { why_param: {} },
398
+ pm,
399
+ };
180
400
  entdesc.path[pathStr].op = entdesc.path[pathStr].op || {};
181
- if (null == entdesc.why_name) {
182
- entdesc.why_name = why_name;
401
+ entdesc.path[pathStr].why_path = why_path;
402
+ ment.entname = entname;
403
+ ment.pm = pm;
404
+ (0, utility_1.debugpath)(pathStr, methodName, 'RESOLVE-ENTITY-NAME', (0, utility_1.formatJSONIC)({ entdesc, ment }, { hsepd: 0, $: true, color: true }));
405
+ }
406
+ function RenameParams(spec) {
407
+ const ctx = spec.ctx;
408
+ const data = spec.data;
409
+ const guide = data.guide;
410
+ const metrics = guide.metrics;
411
+ const mdesc = spec.node.val;
412
+ const ment = mdesc.MethodEntity;
413
+ const pathStr = mdesc.path;
414
+ const work = spec.data.work;
415
+ const entname = mdesc.MethodEntity.entname;
416
+ const entdesc = work.entmap[entname];
417
+ const pathdesc = spec.data.work.pathmap[pathStr];
418
+ const methodName = mdesc.method;
419
+ // Rewrite path parameters that are identifiers to follow the rules:
420
+ // 0. Parameters named [a-z]?id are considered identifiers
421
+ // 1. last identifier is always {id} as this is the primary entity
422
+ // 2. internal identifiers are formatted as {name_id} where name is the parent entity name
423
+ // Example: /api/bar/{id}/zed/{zid}/foo/{fid} ->
424
+ // /api/bar/{bar_id}/zed/{zed_id}/foo/{id}
425
+ // id needs to be t/p/
426
+ const multParamEndMatch = (0, utility_1.pathMatch)(mdesc.path, 'p/p/');
427
+ if (multParamEndMatch) {
428
+ return;
183
429
  }
184
- return entdesc;
430
+ const pathDesc = entdesc.path[pathStr];
431
+ pathDesc.rename = (pathDesc.rename ?? { param: {} });
432
+ pathDesc.why_rename = (pathDesc.why_rename ?? { why_param: {} });
433
+ pathDesc.action = (pathDesc.action ?? {});
434
+ pathDesc.why_action = (pathDesc.why_action ?? {});
435
+ const paramRenameCapture = {
436
+ rename: pathDesc.rename.param = (pathDesc.rename.param ?? {}),
437
+ why: pathDesc.why_rename.why_param = (pathDesc.why_rename.why_param ?? {}),
438
+ };
439
+ const parts = pathdesc.parts;
440
+ const cmpname = mdesc.cmp;
441
+ const considerCmp = null != cmpname &&
442
+ 0 < metrics.count.uniqschema &&
443
+ mdesc.method_rate < IS_ENTCMP_METHOD_RATE;
444
+ const origParams = [];
445
+ for (let partI = 0; partI < parts.length; partI++) {
446
+ let partStr = parts[partI];
447
+ if (isParam(partStr)) {
448
+ origParams.push(partStr.replace(/[\}\{\*]/g, ''));
449
+ const why = [];
450
+ const oldParam = partStr.substring(1, partStr.length - 1);
451
+ paramRenameCapture.why[oldParam] = (paramRenameCapture.why[oldParam] ?? []);
452
+ const lastPart = partI === parts.length - 1;
453
+ const secondLastPart = partI === parts.length - 2;
454
+ const notLastPart = partI < parts.length - 1;
455
+ const hasParent = 0 < partI && !isParam(parts[partI - 1]);
456
+ const parentName = hasParent ? (0, utility_1.canonize)(parts[partI - 1]) : null;
457
+ const not_exact_id = 'id' !== oldParam;
458
+ const probably_an_id = oldParam.endsWith('id')
459
+ || oldParam.endsWith('Id')
460
+ || (0, utility_1.canonize)(oldParam) === parentName;
461
+ (0, utility_1.debugpath)(pathStr, mdesc.method, 'RENAME-PARAM-PART', parts, partI, partStr, {
462
+ lastPart,
463
+ secondLastPart,
464
+ notLastPart,
465
+ hasParent,
466
+ parentName,
467
+ not_exact_id,
468
+ probably_an_id,
469
+ });
470
+ // Id-like not at end, and after a possible entname.
471
+ // .../parentent/{id}/...
472
+ if (probably_an_id
473
+ && hasParent
474
+ && notLastPart) {
475
+ why.push('maybe-parent');
476
+ // actually an action
477
+ if (secondLastPart
478
+ && ((parentName !== entdesc.name
479
+ && entdesc.name.startsWith(parentName + '_'))
480
+ // || parentName === cmp.name
481
+ || parentName === cmpname)) {
482
+ // let newParamName = 'id'
483
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'action-parent:' + entdesc.name);
484
+ why.push('action');
485
+ updateAction(methodName, oldParam, parts[partI + 1], entdesc, pathDesc, 'action-not-parent');
486
+ }
487
+ else if (hasParent && parentName === cmpname) {
488
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'id-parent-cmp');
489
+ why.push('id-parent-cmp');
490
+ }
491
+ else if (hasParent && parentName === entdesc.name) {
492
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'id-parent-ent');
493
+ why.push('id-parent-ent');
494
+ }
495
+ else {
496
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, parentName + '_id', 'parent:' + parentName);
497
+ why.push('parent');
498
+ }
499
+ }
500
+ // /api/foo/{foo}/bar/...
501
+ // param matches parent entname, but is not _id format
502
+ // At end, but not called id.
503
+ // .../ent/{not-id}
504
+ else if (lastPart
505
+ && not_exact_id
506
+ && (!hasParent
507
+ || (parentName === entdesc.name
508
+ || entdesc.name.endsWith('_' + parentName)))
509
+ && (!considerCmp || cmpname === entdesc.name)) {
510
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'end-id;' + methodName + ';parent=' + hasParent + '/' + parentName +
511
+ ';cmp=' + considerCmp + (null == cmpname ? '' : '/' + cmpname));
512
+ why.push('end-id');
513
+ }
514
+ // Mot at end, has preceding non-param part.
515
+ // .../parentent/{paramname}/...
516
+ else if (notLastPart
517
+ && 1 < partI
518
+ && hasParent) {
519
+ why.push('has-parent');
520
+ // Actually primary ent with an action$ suffix
521
+ if (secondLastPart) {
522
+ why.push('second-last');
523
+ if ('id' !== oldParam
524
+ // && fixEntName(partStr) === entdesc.name
525
+ && (0, utility_1.canonize)(partStr) === entdesc.name) {
526
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'end-action');
527
+ why.push('end-action');
528
+ updateAction(methodName, oldParam, parts[partI + 1], entdesc, pathDesc, 'end-action');
529
+ }
530
+ else {
531
+ why.push('not-end-action');
532
+ }
533
+ }
534
+ // Primary ent id not at end!
535
+ else if (hasParent
536
+ && parentName === cmpname) {
537
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, 'id', 'id-not-last');
538
+ why.push('id-not-last');
539
+ // paramRenames[oldParam] = 'id'
540
+ // paramRenamesWhy[oldParam].push('id-not-last')
541
+ }
542
+ // Not primary ent.
543
+ else {
544
+ why.push('default');
545
+ let newParamName = parentName + '_id';
546
+ if (newParamName != oldParam) {
547
+ updateParamRename(ctx, data, pathStr, methodName, paramRenameCapture, oldParam, newParamName, 'not-primary');
548
+ why.push('not-primary');
549
+ // paramRenames[oldParam] = newParamName
550
+ // paramRenamesWhy[oldParam].push('not-primary')
551
+ }
552
+ }
553
+ }
554
+ why.push('done');
555
+ if (paramRenameCapture.rename[oldParam] === oldParam) {
556
+ why.push('delete-dup');
557
+ delete paramRenameCapture.rename[oldParam];
558
+ delete paramRenameCapture.why[oldParam];
559
+ }
560
+ // TODO: these need to done via an API
561
+ (0, utility_1.debugpath)(pathStr, methodName, 'RENAME-PARAM', {
562
+ pathStr,
563
+ methodName,
564
+ partStr,
565
+ why,
566
+ oldParam,
567
+ lastPart,
568
+ secondLastPart,
569
+ notLastPart,
570
+ hasParent,
571
+ parentName,
572
+ not_exact_id,
573
+ probably_an_id,
574
+ considerCmp,
575
+ cmp: mdesc.cmp,
576
+ cmpname,
577
+ paramRenameCapture,
578
+ entdesc
579
+ });
580
+ }
581
+ }
582
+ ment.rename = paramRenameCapture.rename;
583
+ ment.why_rename = paramRenameCapture.why;
584
+ ment.rename_orig = origParams;
185
585
  }
186
- const REQKIND = {
187
- get: 'res',
188
- post: 'req',
189
- put: 'req',
190
- patch: 'req',
191
- };
192
- function resolveComponentName(entname, methodDef, methodStr, pathStr, why_name) {
193
- let compname = undefined;
194
- let xrefs = (0, utility_1.find)(methodDef, 'x-ref')
195
- .filter(xref => xref.val.includes('schema'))
196
- // TODO: identify non-ent schemas
197
- .filter(xref => !xref.val.includes('Meta'))
198
- .sort((a, b) => a.path.length - b.path.length);
199
- // console.log('RCN', pathStr, methodStr, xrefs.map(x => [x.val, x.path.length]))
200
- let first = xrefs[0]?.val;
201
- if (null != first) {
202
- let xrefm = first.match(/\/components\/schemas\/(.+)$/);
203
- if (xrefm) {
204
- why_name.push('cmp');
205
- compname = xrefm[1];
206
- }
207
- }
208
- if (null != compname) {
209
- compname = (0, utility_1.depluralize)((0, jostraca_1.snakify)(compname));
210
- // Assume sub schemas suffixes are not real entities
211
- if (compname.includes(entname)) {
212
- compname = compname.slice(0, compname.indexOf(entname) + entname.length);
213
- }
214
- }
215
- return compname;
216
- }
217
- function resolveOpName(methodStr, methodDef, pathStr, entdesc, why) {
218
- // console.log('ROP', pathStr, methodDef)
219
- let opname = METHOD_IDOP[methodStr];
586
+ function FindActions(spec) {
587
+ const mdesc = spec.node.val;
588
+ const pathStr = mdesc.path;
589
+ const work = spec.data.work;
590
+ const ment = mdesc.MethodEntity;
591
+ const entname = ment.entname;
592
+ const entdesc = work.entmap[entname];
593
+ // const pathdesc = spec.data.work.pathmap[pathStr]
594
+ const pathdesc = entdesc.path[pathStr];
595
+ const methodName = mdesc.method;
596
+ pathdesc.action = (pathdesc.action ?? {});
597
+ pathdesc.why_action = (pathdesc.why_action ?? {});
598
+ const parts = spec.data.work.pathmap[pathStr].parts;
599
+ const fourthLastPart = parts[parts.length - 4];
600
+ const fourthLastPartCanon = (0, utility_1.canonize)(fourthLastPart);
601
+ const thirdLastPart = parts[parts.length - 3];
602
+ const thirdLastPartCanon = (0, utility_1.canonize)(thirdLastPart);
603
+ const secondLastPart = parts[parts.length - 2];
604
+ const secondLastPartCanon = (0, utility_1.canonize)(secondLastPart);
605
+ const lastPart = parts[parts.length - 1];
606
+ const lastPartCanon = (0, utility_1.canonize)(lastPart);
607
+ const cmp = ment.cmp;
608
+ // /api/foo/bar where foo is the entity and bar is the action, no id param
609
+ if (secondLastPartCanon === cmp
610
+ || secondLastPartCanon === ment.origcmp
611
+ || secondLastPartCanon === entname) {
612
+ if (!isParam(lastPart)) {
613
+ updateAction(methodName, lastPart, lastPartCanon, entdesc, pathdesc, 'no-param');
614
+ }
615
+ }
616
+ // /api/foo/{param}/action
617
+ else if (thirdLastPartCanon === cmp
618
+ || thirdLastPartCanon === ment.origcmp
619
+ || thirdLastPartCanon === entname) {
620
+ if (isParam(secondLastPart) && !isParam(lastPart)) {
621
+ updateAction(methodName, lastPart, lastPartCanon, entdesc, pathdesc, 'ent-param-2nd-last');
622
+ }
623
+ }
624
+ // /api/foo/{param}/action/subaction
625
+ else if (fourthLastPartCanon === cmp
626
+ || fourthLastPartCanon === ment.origcmp
627
+ || fourthLastPartCanon === entname) {
628
+ if (isParam(thirdLastPart) && !isParam(secondLastPart) && !isParam(lastPart)) {
629
+ const oldActionName = secondLastPart + '/' + lastPart;
630
+ const actionName = secondLastPartCanon + '_' + lastPartCanon;
631
+ updateAction(methodName, oldActionName, actionName, entdesc, pathdesc, 'ent-param-3rd-last');
632
+ }
633
+ }
634
+ (0, utility_1.debugpath)(pathStr, methodName, 'FIND-ACTIONS', cmp, parts, pathdesc.action, pathdesc.why_action);
635
+ // return pathdesc.action
636
+ }
637
+ function ResolveOperation(spec) {
638
+ const mdesc = spec.node.val;
639
+ const ment = mdesc.MethodEntity;
640
+ const pathStr = mdesc.path;
641
+ const work = spec.data.work;
642
+ const parts = work.pathmap[pathStr].parts;
643
+ const entname = mdesc.MethodEntity.entname;
644
+ const entdesc = work.entmap[entname];
645
+ const methodName = mdesc.method;
646
+ const why_op = ment.why_op = [];
647
+ let opname = METHOD_IDOP[methodName];
648
+ let standard_opname = opname;
220
649
  if (null == opname) {
221
- why.push('no-op:' + methodStr);
650
+ why_op.push('no-op:' + methodName);
222
651
  return;
223
652
  }
224
- if ('load' === opname) {
225
- const islist = isListResponse(methodDef, pathStr, entdesc, why);
653
+ // REVIEW: using POST and PUT in non-restian ways is too wierd to handle consistently
654
+ // correct using guide customizations
655
+ // Sometimes POST is used to update, not create. Attempt to identify this.
656
+ // And sometimes vice versa for PUT
657
+ // const id_param_offset = ment.pm?.expr?.endsWith('/t/') ? 1 : 0
658
+ // const has_end_id_param =
659
+ // entname == canonize(parts[parts.length - 2 - id_param_offset])
660
+ // && parts[parts.length - 1 - id_param_offset]?.toLowerCase().endsWith('id}')
661
+ if ('load' === standard_opname) {
662
+ const islist = isListResponse(mdesc, pathStr, why_op);
226
663
  opname = islist ? 'list' : opname;
227
- // console.log('ISLIST', entdesc.name, methodStr, opname, pathStr)
228
664
  }
665
+ /*
666
+ else if (
667
+ 'create' === standard_opname
668
+ && has_end_id_param
669
+ ) {
670
+ opname = 'update'
671
+ why_op.push('id-present')
672
+ }
673
+
674
+ else if (
675
+ 'update' === standard_opname
676
+ && !has_end_id_param
677
+ ) {
678
+ opname = 'create'
679
+ why_op.push('no-id-present')
680
+ }
681
+ */
229
682
  else {
230
- why.push('not-load');
683
+ why_op.push('not-load');
684
+ }
685
+ // why.push('ent=' + entdesc.name)
686
+ ment.opname = opname;
687
+ ment.why_opname = why_op;
688
+ const op = entdesc.path[pathStr].op;
689
+ const opdef = {
690
+ method: methodName,
691
+ why_op: why_op.join(';')
692
+ };
693
+ if (null == op[opname]) {
694
+ op[opname] = opdef;
231
695
  }
232
- return opname;
696
+ // Conflicting methods for same operation
697
+ // METHOD_CONSIDER_ORDER wins
698
+ // Add operation using method name
699
+ else {
700
+ op[methodName.toLowerCase()] = opdef;
701
+ }
702
+ (0, utility_1.debugpath)(pathStr, methodName, 'ResolveOperation', standard_opname, opname, why_op, op);
233
703
  }
234
- function isListResponse(methodDef, pathStr, entdesc, why) {
235
- const caught = (0, utility_1.capture)(methodDef, {
236
- responses: {
237
- '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
704
+ function ResolveTransform(spec) {
705
+ const mdesc = spec.node.val;
706
+ const ment = mdesc.MethodEntity;
707
+ const pathStr = mdesc.path;
708
+ const work = spec.data.work;
709
+ const entname = mdesc.MethodEntity.entname;
710
+ const entdesc = work.entmap[entname];
711
+ // const pathdesc = spec.data.work.pathmap[pathStr]
712
+ const pathdesc = entdesc.path[pathStr];
713
+ const methodName = mdesc.method;
714
+ const opname = ment.opname;
715
+ const op = pathdesc.op;
716
+ const transform = {
717
+ req: undefined,
718
+ res: undefined,
719
+ };
720
+ const resokdef = mdesc.responses?.[200] || mdesc.responses?.[201];
721
+ const resprops = getResponseSchema(resokdef)?.properties;
722
+ (0, utility_1.debugpath)(pathStr, methodName, 'TRANSFORM-RES', (0, struct_1.keysof)(resprops));
723
+ if (resprops) {
724
+ if (resprops[entdesc.origname]) {
725
+ transform.res = '`body.' + entdesc.origname + '`';
726
+ }
727
+ else if (resprops[entdesc.name]) {
728
+ transform.res = '`body.' + entdesc.name + '`';
238
729
  }
730
+ }
731
+ const reqprops = getRequestBodySchema(mdesc.requestBody);
732
+ (0, utility_1.debugpath)(pathStr, methodName, 'TRANSFORM-REQ', (0, struct_1.keysof)(reqprops));
733
+ if (reqprops) {
734
+ if (reqprops[entdesc.origname]) {
735
+ transform.req = { [entdesc.origname]: '`reqdata`' };
736
+ }
737
+ else if (reqprops[entdesc.origname]) {
738
+ transform.req = { [entdesc.origname]: '`reqdata`' };
739
+ }
740
+ }
741
+ if (!(0, struct_1.isempty)(transform)) {
742
+ op[opname].transform = transform;
743
+ }
744
+ }
745
+ function BuildEntity(spec) {
746
+ const entdesc = spec.node.val;
747
+ // console.log('BUILD-ENTITY')
748
+ // console.dir(entdesc, { depth: null })
749
+ const guide = spec.data.guide;
750
+ guide.metrics.count.entity++;
751
+ const entityMap = guide.entity;
752
+ const path = {};
753
+ const rename_param = (pathdesc) => {
754
+ // console.log('RENAME-PATHDESC', pathdesc)
755
+ const out = {};
756
+ (0, jostraca_1.each)(pathdesc.rename.param, (item) => {
757
+ out[item.key$] = {
758
+ target: item.val$,
759
+ why_rename: pathdesc.why_rename.why_param[item.key$]
760
+ };
761
+ });
762
+ return out;
763
+ };
764
+ (0, jostraca_1.each)(entdesc.path, (pathdesc, pathstr) => {
765
+ const guidepath = {
766
+ why_path: pathdesc.why_path,
767
+ action: pathdesc.action,
768
+ rename: {
769
+ param: rename_param(pathdesc)
770
+ },
771
+ op: pathdesc.op
772
+ };
773
+ path[pathstr] = guidepath;
239
774
  });
240
- const schema = caught.schema;
775
+ entityMap[entdesc.name] = {
776
+ name: entdesc.name,
777
+ orig: entdesc.origcmp,
778
+ path,
779
+ };
780
+ }
781
+ function entityPathMatch_tpte(data, pm, mdesc, why) {
782
+ const ment = mdesc.MethodEntity;
783
+ const pathNameIndex = 2;
784
+ why.push('path=t/p/t/');
785
+ const origPathName = pm[pathNameIndex];
786
+ let entname = (0, utility_1.canonize)(origPathName);
787
+ let ecm = undefined;
788
+ if (null != ment.cmp) {
789
+ ecm = entityCmpMatch(data, entname, mdesc, why);
790
+ entname = ecm.name;
791
+ why.push('has-cmp=' + ecm.orig);
792
+ }
793
+ else if (probableEntityMethod(data, ment, pm, why)) {
794
+ ecm = entityCmpMatch(data, entname, mdesc, why);
795
+ if (ecm.cmpish) {
796
+ entname = ecm.name;
797
+ why.push('prob-ent=' + ecm.orig);
798
+ }
799
+ else if (endsWithCmp(data, pm)) {
800
+ entname = (0, utility_1.canonize)((0, struct_1.getelem)(pm, -1));
801
+ why.push('prob-ent-last=' + ecm.orig);
802
+ }
803
+ else if (0 < (0, utility_1.findPathsWithPrefix)(data, pm.path, { strict: true })) {
804
+ entname = (0, utility_1.canonize)((0, struct_1.getelem)(pm, -1));
805
+ why.push('prob-ent-prefix=' + ecm.orig);
806
+ }
807
+ else {
808
+ entname = (0, utility_1.canonize)((0, struct_1.getelem)(pm, -3)) + '_' + entname;
809
+ why.push('prob-ent-part');
810
+ }
811
+ }
812
+ // Probably an entity action suffix
813
+ else {
814
+ why.push('prob-ent-act');
815
+ entname = (0, utility_1.canonize)((0, struct_1.getelem)(pm, -3));
816
+ }
817
+ return entname;
818
+ }
819
+ function endsWithCmp(data, pm) {
820
+ const last = (0, utility_1.canonize)((0, struct_1.getelem)(pm, -1));
821
+ return isOrigCmp(data, last);
822
+ }
823
+ function isOrigCmp(data, name) {
824
+ return null != data.metrics.count.origcmprefs[name];
825
+ }
826
+ function entityOccursInPath(parts, entname) {
827
+ let partsLower = parts.map(p => p.toLowerCase());
828
+ partsLower = partsLower.filter(p => '{' !== p[0]).map(p => (0, utility_1.canonize)(p));
829
+ return !partsLower.reduce((a, p) => (a && p !== entname), true);
830
+ }
831
+ function entityPathMatch_tpe(data, pm, mdesc, why) {
832
+ const ment = mdesc.MethodEntity;
833
+ const pathNameIndex = 0;
834
+ why.push('path=t/p/');
835
+ const origPathName = pm[pathNameIndex];
836
+ // let entname = fixEntName(origPathName)
837
+ let entname = (0, utility_1.canonize)(origPathName);
838
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
839
+ let ecm = entityCmpMatch(data, entname, mdesc, why);
840
+ entname = ecm.name;
841
+ }
842
+ else {
843
+ why.push('ent-act');
844
+ }
845
+ return entname;
846
+ }
847
+ function entityPathMatch_pte(data, pm, mdesc, why) {
848
+ const ment = mdesc.MethodEntity;
849
+ const pathNameIndex = 1;
850
+ why.push('path=p/t/');
851
+ const origPathName = pm[pathNameIndex];
852
+ let entname = (0, utility_1.canonize)(origPathName);
853
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
854
+ let ecm = entityCmpMatch(data, entname, mdesc, why);
855
+ entname = ecm.name;
856
+ }
857
+ else {
858
+ why.push('ent-act');
859
+ }
860
+ return entname;
861
+ }
862
+ function entityPathMatch_te(data, pm, mdesc, why) {
863
+ const ment = mdesc.MethodEntity;
864
+ const pathNameIndex = 0;
865
+ why.push('path=t/');
866
+ const origPathName = pm[pathNameIndex];
867
+ // let entname = fixEntName(origPathName)
868
+ let entname = (0, utility_1.canonize)(origPathName);
869
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
870
+ let ecm = entityCmpMatch(data, entname, mdesc, why);
871
+ entname = ecm.name;
872
+ }
873
+ else {
874
+ why.push('ent-act');
875
+ }
876
+ return entname;
877
+ }
878
+ function entityPathMatch_tpp(data, pm, mdesc, why) {
879
+ const ment = mdesc.MethodEntity;
880
+ const pathNameIndex = 0;
881
+ why.push('path=t/p/p');
882
+ const origPathName = pm[pathNameIndex];
883
+ // let entname = fixEntName(origPathName)
884
+ let entname = (0, utility_1.canonize)(origPathName);
885
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
886
+ let ecm = entityCmpMatch(data, entname, mdesc, why);
887
+ entname = ecm.name;
888
+ }
889
+ else {
890
+ why.push('ent-act');
891
+ }
892
+ return entname;
893
+ }
894
+ function getRequestBodySchema(requestBody) {
895
+ return requestBody?.content?.['application/json']?.schema ??
896
+ requestBody?.schema;
897
+ }
898
+ function getResponseSchema(response) {
899
+ return response?.content?.['application/json']?.schema ??
900
+ response?.schema;
901
+ }
902
+ // No entity component was found, but there still might be an entity.
903
+ function probableEntityMethod(data, mdesc, pm, why) {
904
+ const request = mdesc.requestBody;
905
+ const reqSchema = getRequestBodySchema(request);
906
+ const response = mdesc.responses?.['201'] || mdesc.responses?.['200'];
907
+ const resSchema = getResponseSchema(response);
908
+ const noResponse = null == resSchema && null != mdesc.responses?.['204'];
909
+ let prob_why = '';
910
+ let probent = false;
911
+ if (noResponse) {
912
+ // No response at all means not an action, thus probably an entity.
913
+ prob_why = 'nores';
914
+ probent = true;
915
+ }
916
+ else if (null != reqSchema) {
917
+ if ('POST' === mdesc.method
918
+ && !pm.expr.endsWith('/p/')
919
+ // A real entity would probably occur in at least one other t/p path
920
+ // otherwise this is probably an action
921
+ && (1 < Object.keys(data.def.paths).filter(path => path.includes('/' + pm[pm.length - 1] + '/')).length)) {
922
+ prob_why = 'post';
923
+ probent = true;
924
+ }
925
+ else if (('PUT' === mdesc.method || 'PATCH' === mdesc.method)
926
+ && pm.expr.endsWith('/p/')) {
927
+ prob_why = 'putish';
928
+ probent = true;
929
+ }
930
+ }
931
+ else if ('GET' === mdesc.method) {
932
+ prob_why = 'get';
933
+ probent = true;
934
+ }
935
+ const rescodes = Object.keys(mdesc.responses ?? {});
936
+ (0, utility_1.debugpath)(mdesc.path, mdesc.method, 'PROBABLE-ENTITY-RESPONSE', { mdesc, responses: rescodes, probent, prob_why });
937
+ why.push('entres=' + probent + '/' + rescodes + ('' === prob_why ? '' : '/' + prob_why));
938
+ return probent;
939
+ }
940
+ function entityCmpMatch(data, entname, mdesc, why) {
941
+ const ment = mdesc.MethodEntity;
942
+ let out = {
943
+ name: entname,
944
+ orig: ment.origcmp ?? entname,
945
+ cmpish: false,
946
+ pathish: true,
947
+ };
948
+ // console.log('ECM-A', out, ment)
949
+ const cmpInfrequent = (ment.method_rate < IS_ENTCMP_METHOD_RATE
950
+ || ment.path_rate < IS_ENTCMP_PATH_RATE);
951
+ if (null != ment.cmp
952
+ && entname != ment.cmp
953
+ && !ment.cmp.startsWith(entname)) {
954
+ if (cmpInfrequent) {
955
+ why.push('cmp-primary');
956
+ out.name = ment.cmp;
957
+ out.orig = ment.origcmp;
958
+ out.cmpish = true;
959
+ out.pathish = false;
960
+ why.push('cmp-infreq');
961
+ }
962
+ else if (cmpOccursInPath(data, ment.cmp)) {
963
+ why.push('cmp-path');
964
+ out.name = ment.cmp;
965
+ out.orig = ment.origcmp;
966
+ out.cmpish = true;
967
+ out.pathish = false;
968
+ why.push('cmp-inpath');
969
+ }
970
+ else {
971
+ why.push('path-over-cmp');
972
+ }
973
+ }
974
+ else if ('DELETE' === mdesc.method
975
+ && null == ment.cmp) {
976
+ let cmps = findcmps(data, mdesc.path, ['responses'], { uniq: true });
977
+ if (1 === cmps.length) {
978
+ out.name = cmps[0].cmp;
979
+ out.orig = cmps[0].origcmp;
980
+ out.cmpish = true;
981
+ out.pathish = false;
982
+ why.push('cmp-found-delete');
983
+ }
984
+ else {
985
+ why.push('path-primary-delete');
986
+ }
987
+ }
988
+ else {
989
+ why.push('path-primary');
990
+ }
991
+ (0, utility_1.debugpath)(mdesc.path, mdesc.method, 'ENTITY-CMP-NAME', mdesc.path, mdesc.method, entname + '->', out, why, ment, IS_ENTCMP_METHOD_RATE, IS_ENTCMP_PATH_RATE);
992
+ // console.log('ECM-Z', out, why, ment)
993
+ return out;
994
+ }
995
+ function cmpOccursInPath(data, cmpname) {
996
+ if (null == data.work.potentialCmpsFromPaths) {
997
+ data.work.potentialCmpsFromPaths = {};
998
+ (0, jostraca_1.each)(data.def.paths, (_pathdef, pathstr) => {
999
+ const parts = data.work.pathmap[pathstr].parts;
1000
+ parts
1001
+ .filter(p => !p.startsWith('{'))
1002
+ .map(p => data.work.potentialCmpsFromPaths[(0, utility_1.canonize)(p)] = true);
1003
+ });
1004
+ }
1005
+ return null != data.work.potentialCmpsFromPaths[cmpname];
1006
+ }
1007
+ function isListResponse(mdesc, pathStr, why) {
1008
+ const ment = mdesc.MethodEntity;
1009
+ const pm = ment.pm;
241
1010
  let islist = false;
242
- if (null == schema) {
243
- why.push('no-schema');
1011
+ let schema;
1012
+ if (pm && pm.expr.endsWith('p/')) {
1013
+ why.push('end-param');
244
1014
  }
245
1015
  else {
246
- if (schema.type === 'array') {
247
- why.push('array');
248
- islist = true;
249
- }
250
- if (!islist) {
251
- const properties = schema.properties || {};
252
- (0, jostraca_1.each)(properties, (prop) => {
253
- if (prop.type === 'array') {
254
- if (1 === (0, struct_1.size)(properties)) {
255
- why.push('one-prop:' + prop.key$);
256
- islist = true;
257
- }
258
- if (2 === (0, struct_1.size)(properties) &&
259
- ('data' === prop.key$ ||
260
- 'list' === prop.key$)) {
261
- why.push('two-prop:' + prop.key$);
262
- islist = true;
263
- }
264
- if (prop.key$ === entdesc.name) {
265
- why.push('name:' + entdesc.origname);
266
- islist = true;
267
- }
268
- if (prop.key$ === entdesc.origname) {
269
- why.push('origname:' + entdesc.origname);
270
- islist = true;
271
- }
272
- const listent = listedEntity(prop);
273
- if (listent === entdesc.name) {
274
- why.push('listent:' + listent);
1016
+ let caught = (0, utility_1.capture)(mdesc, {
1017
+ responses:
1018
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1019
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1020
+ { content: { 'application/json': { schema: '`$CAPTURE`' } } }],
1021
+ });
1022
+ schema = caught.schema;
1023
+ if (null == schema) {
1024
+ caught = (0, utility_1.capture)(mdesc, {
1025
+ responses:
1026
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1027
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1028
+ { schema: '`$CAPTURE`' }],
1029
+ });
1030
+ schema = caught.schema;
1031
+ }
1032
+ if (null == schema) {
1033
+ why.push('no-schema');
1034
+ }
1035
+ else {
1036
+ if (schema.type === 'array') {
1037
+ why.push('array');
1038
+ islist = true;
1039
+ }
1040
+ if (!islist) {
1041
+ const properties = resolveSchemaProperties(schema);
1042
+ (0, jostraca_1.each)(properties, (prop) => {
1043
+ if (prop.type === 'array') {
1044
+ why.push('array-prop:' + prop.key$);
275
1045
  islist = true;
276
1046
  }
277
- // if ('/v2/users' === pathStr) {
278
- // console.log('islistresponse', islist, pathStr, entdesc.name, listedEntity(prop), properties)
279
- // }
280
- }
281
- });
1047
+ });
1048
+ }
1049
+ if (!islist) {
1050
+ why.push('not-list');
1051
+ }
1052
+ }
1053
+ }
1054
+ (0, utility_1.debugpath)(pathStr, mdesc.method, 'IS-LIST', islist, why, schema);
1055
+ return islist;
1056
+ }
1057
+ function resolveSchemaProperties(schema) {
1058
+ let properties = {};
1059
+ // This is definitely heuristic!
1060
+ if (schema.allOf) {
1061
+ for (let i = schema.allOf.length - 1; -1 < i; --i) {
1062
+ properties = (0, struct_1.merge)([properties, schema.allOf[i].properties || {}]);
282
1063
  }
283
- if (!islist) {
284
- why.push('not-list');
1064
+ }
1065
+ if (schema.properties) {
1066
+ properties = (0, struct_1.merge)([properties, schema.properties]);
1067
+ }
1068
+ return properties;
1069
+ }
1070
+ function updateAction(methodName, oldParam, actionName, entityDesc, pathdesc, why) {
1071
+ if (
1072
+ // Entity not already encoding action.
1073
+ !entityDesc.name.endsWith((0, utility_1.canonize)(actionName))
1074
+ && null == pathdesc.action[actionName]) {
1075
+ pathdesc.action[actionName] = {
1076
+ // kind: '`$BOOLEAN`',
1077
+ why_action: ['ent', `${entityDesc.name}`, `${why}`, `${oldParam}`, `${methodName}`]
1078
+ };
1079
+ }
1080
+ }
1081
+ function updateParamRename(ctx, data, path, method, paramRenameCapture, oldParamName, newParamName, why) {
1082
+ const existingNewName = paramRenameCapture.rename[oldParamName];
1083
+ const existingWhy = paramRenameCapture.why[oldParamName];
1084
+ (0, utility_1.debugpath)(path, method, 'UPDATE-PARAM-RENAME', path, oldParamName, newParamName, existingNewName);
1085
+ if (null == existingNewName) {
1086
+ paramRenameCapture.rename[oldParamName] = newParamName;
1087
+ if (!existingWhy.includes(why)) {
1088
+ existingWhy.push(why);
285
1089
  }
1090
+ }
1091
+ else if (newParamName == existingNewName) {
1092
+ // if (!existingWhy.includes(why)) {
1093
+ // existingWhy.push(why)
286
1094
  // }
287
1095
  }
288
- return islist;
1096
+ else {
1097
+ ctx.warn({
1098
+ paramRenameCapture, oldParamName, newParamName, why,
1099
+ note: 'Param rename mismatch: existing: ' +
1100
+ oldParamName + ' -> ' + existingNewName + ' (why: ' + existingNewName + ') ' +
1101
+ ' proposed: ' + newParamName + ' (why: ' + why + ') ' +
1102
+ 'for path: ' + path + '. method: ' + method
1103
+ });
1104
+ }
289
1105
  }
290
- function listedEntity(prop) {
291
- const xref = prop?.items?.['x-ref'];
292
- const m = 'string' === typeof xref && xref.match(/^#\/components\/schemas\/(.+)$/);
293
- if (m) {
294
- return (0, utility_1.depluralize)((0, jostraca_1.snakify)(m[1]));
1106
+ function isParam(partStr) {
1107
+ return '{' === partStr[0] && '}' === partStr[partStr.length - 1];
1108
+ }
1109
+ /*
1110
+ function fixEntName(origName: string) {
1111
+ if (null == origName) {
1112
+ return origName
1113
+ }
1114
+ return depluralize(snakify(origName))
1115
+ }
1116
+ */
1117
+ function findcmps(data, pathStr, underprops, opts) {
1118
+ const cmplist = [];
1119
+ const cmpset = new Set();
1120
+ // TODO: cache in ctx.work
1121
+ (0, jostraca_1.each)(data.def.paths[pathStr])
1122
+ .map((md) => {
1123
+ underprops.map((up) => {
1124
+ let found = (0, utility_1.find)(md[up], 'x-ref');
1125
+ found.map((xref) => {
1126
+ // console.log('FINDCMPS', pathStr, (md as any).key$, up, xref.val)
1127
+ let m = xref.val.match(/\/(components\/schemas|definitions)\/(.+)$/);
1128
+ if (m) {
1129
+ cmplist.push(m[2]);
1130
+ cmpset.add(m[2]);
1131
+ }
1132
+ });
1133
+ });
1134
+ });
1135
+ // console.log('FOUNDCMPS', cmps)
1136
+ return (opts?.uniq ? Array.from(cmpset) : cmplist).map(n => ({ cmp: (0, utility_1.canonize)(n), origcmp: n }));
1137
+ }
1138
+ function makeMethodEntityDesc(desc) {
1139
+ let ment = {
1140
+ cmp: desc.cmp ?? null,
1141
+ origcmp: desc.origcmp ?? null,
1142
+ origcmpref: desc.origcmpref ?? null,
1143
+ ref: desc.ref ?? '',
1144
+ why_cmp: desc.why_cmp ?? [],
1145
+ cmpoccur: desc.cmpoccur ?? 0,
1146
+ path_rate: desc.path_rate ?? 0,
1147
+ method_rate: desc.method_rate ?? 0,
1148
+ entname: desc.entname ?? '',
1149
+ why_op: desc.why_op ?? [],
1150
+ rename: desc.rename ?? { param: {} },
1151
+ why_rename: desc.why_rename ?? { why_param: {} },
1152
+ rename_orig: desc.rename_orig ?? [],
1153
+ opname: desc.opname ?? '',
1154
+ why_opname: desc.why_opname ?? [],
1155
+ };
1156
+ return ment;
1157
+ }
1158
+ function findPotentialSchemaRefs(pathStr, methodName, responses) {
1159
+ const xrefs = [];
1160
+ const rescodes = ['200', '201'];
1161
+ for (let rescode of rescodes) {
1162
+ const schema = getResponseSchema(responses[rescode]);
1163
+ if (null != schema) {
1164
+ if (null != schema['x-ref']) {
1165
+ xrefs.push(schema['x-ref']);
1166
+ }
1167
+ else if ('array' === schema.type && null != schema.items?.['x-ref']) {
1168
+ xrefs.push(schema.items?.['x-ref']);
1169
+ }
1170
+ }
295
1171
  }
1172
+ (0, utility_1.debugpath)(pathStr, methodName, 'POTENTIAL-SCHEMA-REFS', xrefs);
1173
+ return xrefs;
1174
+ }
1175
+ function hasMethod(def, pathStr, methodName) {
1176
+ const pathDef = def?.paths?.[pathStr];
1177
+ const found = (null != pathDef
1178
+ && (null != pathDef[methodName.toLowerCase()]
1179
+ || null != pathDef[methodName.toUpperCase()]));
1180
+ console.log('hasMethod', pathStr, methodName, found);
1181
+ return found;
296
1182
  }
297
1183
  //# sourceMappingURL=heuristic01.js.map