spice-js 2.7.35 → 2.7.36

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.
@@ -19,6 +19,50 @@ function getPackageInfo() {
19
19
  var pac = require(_path2.default.join(spice.parent_app_dir, "/package.json"));
20
20
  return pac;
21
21
  }
22
+
23
+ // Swagger 2.0 restricts a parameter to these types. The schema vocabulary is
24
+ // richer ("date", "Array", model names) and anything outside this set makes the
25
+ // parameter invalid, so Swagger UI renders no input for it at all.
26
+ var SWAGGER_PARAMETER_TYPES = ["string", "number", "integer", "boolean", "array", "file"];
27
+ function toParameterType(type) {
28
+ if (typeof type != "string") {
29
+ return {
30
+ type: "string"
31
+ };
32
+ }
33
+ var lowered = type.toLowerCase();
34
+ if (lowered == "date") {
35
+ return {
36
+ type: "string",
37
+ format: "date"
38
+ };
39
+ }
40
+ if (lowered == "datetime") {
41
+ return {
42
+ type: "string",
43
+ format: "date-time"
44
+ };
45
+ }
46
+ // Objects and collections are submitted as text in a form body
47
+ if (_lodash.default.includes(["object", "array"], lowered)) {
48
+ return {
49
+ type: "string"
50
+ };
51
+ }
52
+ if (_lodash.default.includes(SWAGGER_PARAMETER_TYPES, lowered)) {
53
+ return {
54
+ type: lowered
55
+ };
56
+ }
57
+ // Model references and anything unrecognised
58
+ return {
59
+ type: "string"
60
+ };
61
+ }
62
+ function trimTrailingSlash(p) {
63
+ if (typeof p !== "string") return p;
64
+ return p.length > 1 ? p.replace(/\/+$/, "") : p;
65
+ }
22
66
  function createPath(path) {
23
67
  return _lodash.default.join(_lodash.default.map(_lodash.default.split(path, "/"), seg => {
24
68
  if (_lodash.default.startsWith(seg, ":")) {
@@ -92,18 +136,35 @@ function getRoutes(models) {
92
136
  description: "Not found"
93
137
  }
94
138
  };
95
- var comment = _lodash.default.find(comment_processed, comment => {
96
- function matches() {
97
- var match = false;
98
- match = _lodash.default.endsWith(layer.path, comment.original_path);
99
- return match;
100
- }
101
- return comment.method == _lodash.default.lowerCase(_lodash.default.last(layer.methods)) && matches();
139
+
140
+ // koa-router drops the trailing slash when it applies a router
141
+ // prefix, so a route declared as router.post("/") ends up with
142
+ // layer.path equal to the prefix itself and never "ends with" the
143
+ // documented "/". That left every collection-root endpoint without
144
+ // its comment, and so without parameters, description or response
145
+ // schema. Expand the documented path against the prefix and match
146
+ // exactly, keeping the old suffix match as a fallback for files
147
+ // that document fully-qualified paths.
148
+ var layer_method = _lodash.default.lowerCase(_lodash.default.last(layer.methods));
149
+ var layer_path = trimTrailingSlash(layer.path);
150
+ var router_prefix = router.opts && router.opts.prefix || "";
151
+ var candidates = _lodash.default.filter(comment_processed, item => item.method == layer_method);
152
+ var comment = _lodash.default.find(candidates, item => layer_path === trimTrailingSlash(router_prefix + item.original_path)) || _lodash.default.find(candidates, item => {
153
+ var documented = trimTrailingSlash(item.original_path);
154
+ // "/" would suffix-match every path in the file
155
+ if (documented === "/" || documented === "") return false;
156
+ return _lodash.default.endsWith(layer_path, documented);
102
157
  });
103
158
  if (comment != undefined) {
104
159
  _path.description = comment.description;
105
160
  _path.operationId = _uuid.default.v4();
106
161
  _path.parameters = [...preparParameters(comment, undefined, models, router), ..._path.params];
162
+ // Swagger 2.0 requires an operation carrying formData parameters
163
+ // to declare a form media type, otherwise "Try it out" has no
164
+ // body to build and the fields are ignored.
165
+ if (_lodash.default.some(_path.parameters, param => param.in === "formData")) {
166
+ _path.consumes = [_lodash.default.some(_path.parameters, param => param.type === "file") ? "multipart/form-data" : "application/x-www-form-urlencoded"];
167
+ }
107
168
  var doc_return = filterTags(comment, "returns");
108
169
  if (doc_return.length > 0) {
109
170
  if (doc_return[0].type == undefined) {
@@ -166,14 +227,14 @@ function preparParameters(comment, defaultModel, models) {
166
227
  }
167
228
  function modelToParameter(model) {
168
229
  return _lodash.default.map(models[model], (value, key) => {
169
- return {
170
- name: key,
171
- type: _lodash.default.includes(["object", "array"], value.type) ? "string" : value.type,
230
+ return _extends({
231
+ name: key
232
+ }, toParameterType(value.type), {
172
233
  description: value.description ? value.description : "",
173
234
  required: value.required ? value.required : false,
174
235
  paramType: "form",
175
236
  in: "formData"
176
- };
237
+ });
177
238
  }).filter(obj => obj != undefined);
178
239
  }
179
240
  /* console.log("Props::::::: ", [
@@ -326,6 +387,37 @@ function generateTags() {
326
387
  };
327
388
  });
328
389
  }
390
+
391
+ // An @returns tag names a model, but plenty of routes use it for prose
392
+ // ("success status", "Providers + models"). Those become $refs that point at a
393
+ // definition which does not exist, and Swagger UI cannot resolve them, so the
394
+ // response renders blank. Keep the prose as a description instead.
395
+ function pruneDanglingRefs(spec) {
396
+ var available = _lodash.default.keys(spec.definitions || {});
397
+ _lodash.default.each(spec.paths, methods => {
398
+ _lodash.default.each(methods, operation => {
399
+ var response = _lodash.default.get(operation, ["responses", "200"]);
400
+ if (response == undefined || response.schema == undefined) {
401
+ return;
402
+ }
403
+ var schema = response.schema;
404
+ var holder = schema.$ref != undefined ? schema : _lodash.default.get(schema, ["items", "$ref"]) != undefined ? schema.items : _lodash.default.get(schema, ["additionalProperties", "$ref"]) != undefined ? schema.additionalProperties : undefined;
405
+ if (holder == undefined) {
406
+ return;
407
+ }
408
+ var referenced = _lodash.default.last(_lodash.default.split(holder.$ref, "/"));
409
+ if (_lodash.default.includes(available, referenced)) {
410
+ return;
411
+ }
412
+ delete holder.$ref;
413
+ holder.type = "object";
414
+ if (referenced) {
415
+ response.description = "Successful Operation - " + referenced;
416
+ }
417
+ });
418
+ });
419
+ return spec;
420
+ }
329
421
  function generateSpec() {
330
422
  return _generateSpec.apply(this, arguments);
331
423
  }
@@ -366,7 +458,7 @@ function _generateSpec() {
366
458
  paths: generatePaths(routes),
367
459
  definitions: generateDefinitions(models)
368
460
  };
369
- return spec;
461
+ return pruneDanglingRefs(spec);
370
462
  });
371
463
  return _generateSpec.apply(this, arguments);
372
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.35",
3
+ "version": "2.7.36",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -13,6 +13,45 @@ function getPackageInfo() {
13
13
  return pac;
14
14
  }
15
15
 
16
+ // Swagger 2.0 restricts a parameter to these types. The schema vocabulary is
17
+ // richer ("date", "Array", model names) and anything outside this set makes the
18
+ // parameter invalid, so Swagger UI renders no input for it at all.
19
+ const SWAGGER_PARAMETER_TYPES = [
20
+ "string",
21
+ "number",
22
+ "integer",
23
+ "boolean",
24
+ "array",
25
+ "file",
26
+ ];
27
+
28
+ function toParameterType(type) {
29
+ if (typeof type != "string") {
30
+ return { type: "string" };
31
+ }
32
+ let lowered = type.toLowerCase();
33
+ if (lowered == "date") {
34
+ return { type: "string", format: "date" };
35
+ }
36
+ if (lowered == "datetime") {
37
+ return { type: "string", format: "date-time" };
38
+ }
39
+ // Objects and collections are submitted as text in a form body
40
+ if (_.includes(["object", "array"], lowered)) {
41
+ return { type: "string" };
42
+ }
43
+ if (_.includes(SWAGGER_PARAMETER_TYPES, lowered)) {
44
+ return { type: lowered };
45
+ }
46
+ // Model references and anything unrecognised
47
+ return { type: "string" };
48
+ }
49
+
50
+ function trimTrailingSlash(p) {
51
+ if (typeof p !== "string") return p;
52
+ return p.length > 1 ? p.replace(/\/+$/, "") : p;
53
+ }
54
+
16
55
  function createPath(path) {
17
56
  return _.join(
18
57
  _.map(_.split(path, "/"), (seg) => {
@@ -100,17 +139,36 @@ function getRoutes(models) {
100
139
  },
101
140
  };
102
141
 
103
- let comment = _.find(comment_processed, (comment) => {
104
- function matches() {
105
- let match = false;
106
- match = _.endsWith(layer.path, comment.original_path);
107
- return match;
108
- }
109
- return (
110
- comment.method == _.lowerCase(_.last(layer.methods)) &&
111
- matches()
112
- );
113
- });
142
+ // koa-router drops the trailing slash when it applies a router
143
+ // prefix, so a route declared as router.post("/") ends up with
144
+ // layer.path equal to the prefix itself and never "ends with" the
145
+ // documented "/". That left every collection-root endpoint without
146
+ // its comment, and so without parameters, description or response
147
+ // schema. Expand the documented path against the prefix and match
148
+ // exactly, keeping the old suffix match as a fallback for files
149
+ // that document fully-qualified paths.
150
+ let layer_method = _.lowerCase(_.last(layer.methods));
151
+ let layer_path = trimTrailingSlash(layer.path);
152
+ let router_prefix = (router.opts && router.opts.prefix) || "";
153
+
154
+ let candidates = _.filter(
155
+ comment_processed,
156
+ (item) => item.method == layer_method
157
+ );
158
+
159
+ let comment =
160
+ _.find(
161
+ candidates,
162
+ (item) =>
163
+ layer_path ===
164
+ trimTrailingSlash(router_prefix + item.original_path)
165
+ ) ||
166
+ _.find(candidates, (item) => {
167
+ let documented = trimTrailingSlash(item.original_path);
168
+ // "/" would suffix-match every path in the file
169
+ if (documented === "/" || documented === "") return false;
170
+ return _.endsWith(layer_path, documented);
171
+ });
114
172
 
115
173
  if (comment != undefined) {
116
174
  path.description = comment.description;
@@ -119,6 +177,18 @@ function getRoutes(models) {
119
177
  ...preparParameters(comment, undefined, models, router),
120
178
  ...path.params,
121
179
  ];
180
+ // Swagger 2.0 requires an operation carrying formData parameters
181
+ // to declare a form media type, otherwise "Try it out" has no
182
+ // body to build and the fields are ignored.
183
+ if (_.some(path.parameters, (param) => param.in === "formData")) {
184
+ path.consumes = [
185
+ (
186
+ _.some(path.parameters, (param) => param.type === "file")
187
+ ) ?
188
+ "multipart/form-data"
189
+ : "application/x-www-form-urlencoded",
190
+ ];
191
+ }
122
192
  let doc_return = filterTags(comment, "returns");
123
193
  if (doc_return.length > 0) {
124
194
  if (doc_return[0].type == undefined) {
@@ -185,8 +255,7 @@ function preparParameters(comment, defaultModel, models) {
185
255
  return _.map(models[model], (value, key) => {
186
256
  return {
187
257
  name: key,
188
- type:
189
- _.includes(["object", "array"], value.type) ? "string" : value.type,
258
+ ...toParameterType(value.type),
190
259
  description: value.description ? value.description : "",
191
260
  required: value.required ? value.required : false,
192
261
  paramType: "form",
@@ -353,6 +422,42 @@ function generateTags() {
353
422
  });
354
423
  }
355
424
 
425
+ // An @returns tag names a model, but plenty of routes use it for prose
426
+ // ("success status", "Providers + models"). Those become $refs that point at a
427
+ // definition which does not exist, and Swagger UI cannot resolve them, so the
428
+ // response renders blank. Keep the prose as a description instead.
429
+ function pruneDanglingRefs(spec) {
430
+ let available = _.keys(spec.definitions || {});
431
+ _.each(spec.paths, (methods) => {
432
+ _.each(methods, (operation) => {
433
+ let response = _.get(operation, ["responses", "200"]);
434
+ if (response == undefined || response.schema == undefined) {
435
+ return;
436
+ }
437
+ let schema = response.schema;
438
+ let holder =
439
+ schema.$ref != undefined ? schema
440
+ : _.get(schema, ["items", "$ref"]) != undefined ? schema.items
441
+ : _.get(schema, ["additionalProperties", "$ref"]) != undefined ?
442
+ schema.additionalProperties
443
+ : undefined;
444
+ if (holder == undefined) {
445
+ return;
446
+ }
447
+ let referenced = _.last(_.split(holder.$ref, "/"));
448
+ if (_.includes(available, referenced)) {
449
+ return;
450
+ }
451
+ delete holder.$ref;
452
+ holder.type = "object";
453
+ if (referenced) {
454
+ response.description = `Successful Operation - ${referenced}`;
455
+ }
456
+ });
457
+ });
458
+ return spec;
459
+ }
460
+
356
461
  export default async function generateSpec() {
357
462
  let { description, version, name } = getPackageInfo();
358
463
  let models = _.cloneDeep(await getModels());
@@ -392,5 +497,5 @@ export default async function generateSpec() {
392
497
  paths: generatePaths(routes),
393
498
  definitions: generateDefinitions(models),
394
499
  };
395
- return spec;
500
+ return pruneDanglingRefs(spec);
396
501
  }