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.
- package/build/docs/generator.js +113 -16
- package/build/utility/RestHelper.js +3 -1
- package/package.json +1 -1
- package/src/docs/generator.js +128 -18
- package/src/utility/RestHelper.js +6 -1
package/build/docs/generator.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
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]] =
|
|
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.
|
|
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
package/src/docs/generator.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
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]] =
|
|
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.
|
|
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
|
|