spice-js 2.7.35 → 2.7.38

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/build/index.js CHANGED
@@ -189,6 +189,10 @@ class Spice {
189
189
  return spice;
190
190
  } catch (e) {
191
191
  console.log(e.stack);
192
+ // A failed init leaves no usable application (no config, no database,
193
+ // no routes). Surface it so the host process can exit instead of
194
+ // listening on a port with nothing behind it.
195
+ throw e;
192
196
  }
193
197
  })();
194
198
  }
@@ -25,11 +25,22 @@ function _load() {
25
25
  console.log('APP Bootstrap directory', app_bootstrap_path); */
26
26
  console.log("Loading Ordered Files", ordered_modules);
27
27
  for (var file of ordered_modules) {
28
- var mod = require(_path.default.join(bootstrap_path, file));
29
- if (_lodash.default.isFunction(mod)) {
30
- yield mod();
28
+ try {
29
+ var mod = require(_path.default.join(bootstrap_path, file));
30
+ if (_lodash.default.isFunction(mod)) {
31
+ yield mod();
32
+ }
33
+ console.log("loaded", file);
34
+ } catch (e) {
35
+ // The ordered modules are the foundation: database, models, logger.
36
+ // Nothing loaded after them can work without them, so swallowing the
37
+ // error here produced a half-booted process (no routes, no models,
38
+ // no retry) that still looked alive. Mark it and let init() rethrow
39
+ // so the host application can exit and be restarted.
40
+ console.log("Failed to load core module \"" + file + "\"");
41
+ e.spice_core_module = file;
42
+ throw e;
31
43
  }
32
- console.log("loaded", file);
33
44
  }
34
45
  var paths = [bootstrap_path, app_bootstrap_path];
35
46
  for (var loadder_path of paths) {
@@ -52,6 +63,9 @@ function _load() {
52
63
  }
53
64
  } catch (e) {
54
65
  console.log(e.stack);
66
+ if (e.spice_core_module) {
67
+ throw e;
68
+ }
55
69
  }
56
70
  });
57
71
  return _load.apply(this, arguments);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.35",
3
+ "version": "2.7.38",
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
  }
package/src/index.js CHANGED
@@ -166,6 +166,10 @@ export default class Spice {
166
166
  return spice;
167
167
  } catch (e) {
168
168
  console.log(e.stack);
169
+ // A failed init leaves no usable application (no config, no database,
170
+ // no routes). Surface it so the host process can exit instead of
171
+ // listening on a port with nothing behind it.
172
+ throw e;
169
173
  }
170
174
  }
171
175
  }
@@ -26,11 +26,22 @@ export async function load() {
26
26
  console.log('APP Bootstrap directory', app_bootstrap_path); */
27
27
  console.log("Loading Ordered Files", ordered_modules);
28
28
  for (let file of ordered_modules) {
29
- let mod = require(path.join(bootstrap_path, file));
30
- if (_.isFunction(mod)) {
31
- await mod();
29
+ try {
30
+ let mod = require(path.join(bootstrap_path, file));
31
+ if (_.isFunction(mod)) {
32
+ await mod();
33
+ }
34
+ console.log("loaded", file);
35
+ } catch (e) {
36
+ // The ordered modules are the foundation: database, models, logger.
37
+ // Nothing loaded after them can work without them, so swallowing the
38
+ // error here produced a half-booted process (no routes, no models,
39
+ // no retry) that still looked alive. Mark it and let init() rethrow
40
+ // so the host application can exit and be restarted.
41
+ console.log(`Failed to load core module "${file}"`);
42
+ e.spice_core_module = file;
43
+ throw e;
32
44
  }
33
- console.log("loaded", file);
34
45
  }
35
46
 
36
47
  let paths = [bootstrap_path, app_bootstrap_path];
@@ -60,6 +71,9 @@ export async function load() {
60
71
  }
61
72
  } catch (e) {
62
73
  console.log(e.stack);
74
+ if (e.spice_core_module) {
75
+ throw e;
76
+ }
63
77
  }
64
78
  }
65
79
  //load();