spice-js 2.7.34 → 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::::::: ", [
@@ -222,11 +283,16 @@ function _getModels() {
222
283
  var models = {};
223
284
  require("fs").readdirSync(paths).forEach(function (file) {
224
285
  var model = require(_path2.default.join(paths, file));
225
- // Handle ESM default exports
226
- if (model.default) {
227
- model = model.default;
286
+ // `props` is a named export; the default export is the SpiceModel class,
287
+ // which does not carry it. Unwrapping to the default export first drops
288
+ // the schema off every model and leaves Swagger with empty definitions,
289
+ // so read the named export and only fall back to the default export for
290
+ // models that attach props there instead.
291
+ var props = model.props;
292
+ if (props == undefined && model.default != undefined) {
293
+ props = model.default.props;
228
294
  }
229
- models[file.split(".")[0]] = model.props;
295
+ models[file.split(".")[0]] = props;
230
296
  });
231
297
  return models;
232
298
  });
@@ -321,6 +387,37 @@ function generateTags() {
321
387
  };
322
388
  });
323
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
+ }
324
421
  function generateSpec() {
325
422
  return _generateSpec.apply(this, arguments);
326
423
  }
@@ -361,7 +458,7 @@ function _generateSpec() {
361
458
  paths: generatePaths(routes),
362
459
  definitions: generateDefinitions(models)
363
460
  };
364
- return spec;
461
+ return pruneDanglingRefs(spec);
365
462
  });
366
463
  return _generateSpec.apply(this, arguments);
367
464
  }
@@ -36,7 +36,9 @@ class RestHelper {
36
36
  ctx.status = 200;
37
37
  ctx.body = response;
38
38
  } catch (e) {
39
- console.log("Error at response: ", e, e.stack);
39
+ console.error("Error at response: ", e, e == null ? void 0 : e.stack);
40
+ ctx.status = 500;
41
+ ctx.body = RestHelper.prepare_response(RestHelper.FAILURE, (e == null ? void 0 : e.message) || e);
40
42
  }
41
43
  })();
42
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.34",
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",
@@ -244,11 +313,16 @@ async function getModels() {
244
313
  .readdirSync(paths)
245
314
  .forEach(function (file) {
246
315
  let model = require(path.join(paths, file));
247
- // Handle ESM default exports
248
- if (model.default) {
249
- model = model.default;
316
+ // `props` is a named export; the default export is the SpiceModel class,
317
+ // which does not carry it. Unwrapping to the default export first drops
318
+ // the schema off every model and leaves Swagger with empty definitions,
319
+ // so read the named export and only fall back to the default export for
320
+ // models that attach props there instead.
321
+ let props = model.props;
322
+ if (props == undefined && model.default != undefined) {
323
+ props = model.default.props;
250
324
  }
251
- models[file.split(".")[0]] = model.props;
325
+ models[file.split(".")[0]] = props;
252
326
  });
253
327
  return models;
254
328
  }
@@ -348,6 +422,42 @@ function generateTags() {
348
422
  });
349
423
  }
350
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
+
351
461
  export default async function generateSpec() {
352
462
  let { description, version, name } = getPackageInfo();
353
463
  let models = _.cloneDeep(await getModels());
@@ -387,5 +497,5 @@ export default async function generateSpec() {
387
497
  paths: generatePaths(routes),
388
498
  definitions: generateDefinitions(models),
389
499
  };
390
- return spec;
500
+ return pruneDanglingRefs(spec);
391
501
  }
@@ -30,7 +30,12 @@ export default class RestHelper {
30
30
  ctx.status = 200;
31
31
  ctx.body = response;
32
32
  } catch (e) {
33
- console.log("Error at response: ", e, e.stack);
33
+ console.error("Error at response: ", e, e?.stack);
34
+ ctx.status = 500;
35
+ ctx.body = RestHelper.prepare_response(
36
+ RestHelper.FAILURE,
37
+ e?.message || e,
38
+ );
34
39
  }
35
40
  }
36
41