Skip to content

Commit aeda736

Browse files
danielsharveytheoomoregbee
authored andcommitted
fix: Correct shortcut blueprints.
Correct shortcut blueprints; shortcuts have requestBody data supplied as query parameters as a convenience during DEV. Fix association foreign key parameter name in blueprint documentation; was documented as `fk`, should be `childid`. Adjust integer to be 64-bit; all numbers in JavaScript 64 bits. Update test data.
1 parent 8a2a4b5 commit aeda736

File tree

7 files changed

+455
-193
lines changed

7 files changed

+455
-193
lines changed

api/models/User.js

+3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ module.exports = {
7474
collection: "Pet",
7575
via: "owner",
7676
},
77+
favouritePet: {
78+
model: "Pet",
79+
},
7780
neighboursPets: {
7881
collection: "Pet",
7982
via: "caredForBy",

lib/generators.ts

+149-36
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import forEach from 'lodash/forEach';
1212
import { OpenApi } from '../types/openapi';
1313
import set from 'lodash/set';
1414
import { map, omit, isEqual, reduce } from 'lodash';
15-
import { attributeValidations } from './utils';
15+
import { attributeValidations, resolveRef } from './utils';
1616

1717

1818
/**
1919
* Generate Swagger schema content describing the specified Sails attribute.
20-
* TODO: add test to this function
20+
*
21+
* XXX TODO: add test to this function
2122
*
2223
* @see https://swagger.io/docs/specification/data-models/
2324
* @param {Record<string, any>} attribute Sails model attribute specification as per `Model.js` file
@@ -155,31 +156,119 @@ export const generateAttributeSchema = (attribute: Sails.AttributeDefinition): O
155156
* Generate the OpenAPI schemas for the foreign key values used to reference
156157
* ORM records for the associations of the specified Sails Model.
157158
*
159+
* Used for 'replace' REST blueprint.
160+
*
158161
* @param model
159162
* @param models
160163
*/
161-
export const generateModelAssociationFKAttributeSchemas = (model: SwaggerSailsModel, models: NameKeyMap<SwaggerSailsModel>): OpenApi.UpdatedSchema[] => {
164+
export const generateModelAssociationFKAttributeSchemas = (model: SwaggerSailsModel,
165+
aliasesToInclude: string[] | undefined, models: NameKeyMap<SwaggerSailsModel>): OpenApi.UpdatedSchema[] => {
162166

163167
if (!model.associations) { return []; }
164168

165-
const ret = model.associations.map(association => {
169+
return model.associations.map(association => {
170+
171+
if (!aliasesToInclude || aliasesToInclude.indexOf(association.alias) < 0) return;
166172

167173
const targetModelIdentity = association.type === 'model' ? association.model : association.collection;
168174
const targetModel = models[targetModelIdentity!];
169175

170176
if (!targetModel) {
171-
return {}; // data structure integrity issue should not occur
177+
return; // data structure integrity issue should not occur
172178
}
173179

180+
const description = association.type === 'model' ?
181+
`**${model.globalId}** record's foreign key value to use as the replacement for this attribute`
182+
: `**${model.globalId}** record's foreign key values to use as the replacement for this collection`;
183+
174184
const targetFKAttribute = targetModel.attributes[targetModel.primaryKey];
175185
return generateAttributeSchema({
176186
...targetFKAttribute,
177-
description: `**${model.globalId}** record's foreign key value (**${association.alias}** association${targetFKAttribute.description ? '; ' + targetFKAttribute.description : ''})`
187+
autoMigrations: {
188+
...(targetFKAttribute.autoMigrations || {}),
189+
autoIncrement: false, // autoIncrement not relevant for FK parameter
190+
},
191+
description: `${description} (**${association.alias}** association${targetFKAttribute.description ? '; ' + targetFKAttribute.description : ''})`
178192
});
179193

180-
});
194+
})
195+
.filter(parameter => parameter) as OpenApi.UpdatedSchema[];
196+
197+
}
198+
199+
/**
200+
* Generate the OpenAPI parameters for the foreign key values used to reference
201+
* ORM records for the associations of the specified Sails Model.
202+
*
203+
* Used for 'replace' shortcut blueprint.
204+
*
205+
* @param model
206+
* @param aliasesToInclude
207+
* @param models
208+
*/
209+
export const generateModelAssociationFKAttributeParameters = (model: SwaggerSailsModel,
210+
aliasesToInclude: string[] | undefined, models: NameKeyMap<SwaggerSailsModel>): OpenApi.Parameter[] => {
211+
212+
if (!model.associations) { return []; }
213+
214+
return model.associations.map(association => {
215+
216+
if (!aliasesToInclude || aliasesToInclude.indexOf(association.alias) < 0) return;
217+
218+
const targetModelIdentity = association.type === 'model' ? association.model : association.collection;
219+
const targetModel = models[targetModelIdentity!];
220+
221+
if (!targetModel) {
222+
return; // data structure integrity issue should not occur
223+
}
224+
225+
const description = association.type === 'model' ?
226+
`**${model.globalId}** record's foreign key value to use as the replacement for this attribute`
227+
: `**${model.globalId}** record's foreign key values to use as the replacement for this collection`;
228+
229+
const targetFKAttribute = targetModel.attributes[targetModel.primaryKey];
230+
const targetFKAttributeSchema = generateAttributeSchema({
231+
...targetFKAttribute,
232+
autoMigrations: {
233+
...(targetFKAttribute.autoMigrations || {}),
234+
autoIncrement: false, // autoIncrement not relevant for FK parameter
235+
},
236+
description: `${description} (**${association.alias}** association${targetFKAttribute.description ? '; ' + targetFKAttribute.description : ''})`
237+
});
181238

182-
return ret;
239+
return {
240+
in: 'query' as OpenApi.ParameterLocation,
241+
name: association.alias,
242+
description: targetFKAttributeSchema.description,
243+
schema: {
244+
type: 'array',
245+
items: targetFKAttributeSchema,
246+
},
247+
} as OpenApi.Parameter;
248+
249+
})
250+
.filter(parameter => parameter) as OpenApi.Parameter[];
251+
252+
}
253+
254+
export const generateSchemaAsQueryParameters = (schema: OpenApi.UpdatedSchema): OpenApi.Parameter[] => {
255+
256+
const required = schema.required || [];
257+
258+
return map(schema.properties || {}, (property, name) => {
259+
const parameter = {
260+
in: 'query' as OpenApi.ParameterLocation,
261+
name: name,
262+
schema: property,
263+
} as OpenApi.Parameter;
264+
if((property as OpenApi.UpdatedSchema).description) {
265+
parameter.description = (property as OpenApi.UpdatedSchema).description;
266+
}
267+
if(required.indexOf(name) >= 0) {
268+
parameter.required = true;
269+
}
270+
return parameter;
271+
});
183272

184273
}
185274

@@ -237,7 +326,7 @@ export const generateSchemas = (models: NameKeyMap<SwaggerSailsModel>): NameKeyM
237326
* @param models
238327
*/
239328
export const generatePaths = (routes: SwaggerRouteInfo[], templates: BlueprintActionTemplates,
240-
defaultsValues: Defaults, specification: Omit<OpenApi.OpenApi, 'paths'>,
329+
defaultsValues: Defaults, specification: OpenApi.OpenApi,
241330
models: NameKeyMap<SwaggerSailsModel>): OpenApi.Paths => {
242331

243332
const paths = {};
@@ -359,16 +448,29 @@ export const generatePaths = (routes: SwaggerRouteInfo[], templates: BlueprintAc
359448
},
360449

361450
addModelBodyParam: () => {
362-
if (pathEntry.requestBody) return;
363-
pathEntry.requestBody = {
364-
description: subst('JSON dictionary representing the {globalId} instance to create.'),
365-
required: true,
366-
content: {
367-
'application/json': {
368-
schema: { "$ref": "#/components/schemas/" + route.model!.identity }
451+
if(route.actionType === 'shortcutBlueprint') {
452+
const schema = specification!.components!.schemas?.[route.model!.identity];
453+
if (schema) {
454+
const resolvedSchema = resolveRef(specification, schema);
455+
if (resolvedSchema) {
456+
generateSchemaAsQueryParameters(resolvedSchema as OpenApi.UpdatedSchema).map(p => {
457+
if (isParam('query', p.name)) return;
458+
pathEntry.parameters.push(p);
459+
});
460+
}
461+
}
462+
} else {
463+
if (pathEntry.requestBody) return;
464+
pathEntry.requestBody = {
465+
description: subst('JSON dictionary representing the {globalId} instance to create.'),
466+
required: true,
467+
content: {
468+
'application/json': {
469+
schema: { "$ref": "#/components/schemas/" + route.model!.identity }
470+
},
369471
},
370-
},
371-
};
472+
};
473+
}
372474
},
373475

374476
addResultOfArrayOfModels: () => {
@@ -400,13 +502,13 @@ export const generatePaths = (routes: SwaggerRouteInfo[], templates: BlueprintAc
400502
},
401503

402504
addAssociationFKPathParam: () => {
403-
if (isParam('path', 'fk')) return; // pre-defined/pre-configured --> skip
505+
if (isParam('path', 'childid')) return; // pre-defined/pre-configured --> skip
404506
pathEntry.parameters.push({
405507
in: 'path',
406-
name: 'fk',
508+
name: 'childid',
407509
required: true,
408510
schema: {
409-
oneOf: generateModelAssociationFKAttributeSchemas(route.model!, models),
511+
oneOf: generateModelAssociationFKAttributeSchemas(route.model!, route.associationAliases, models),
410512
} as OpenApi.UpdatedSchema,
411513
description: 'The desired target association record\'s foreign key value'
412514
});
@@ -460,26 +562,37 @@ export const generatePaths = (routes: SwaggerRouteInfo[], templates: BlueprintAc
460562
},
461563

462564
addFksBodyParam: () => {
463-
if (pathEntry.requestBody) return;
464-
pathEntry.requestBody = {
465-
description: 'The primary key values (usually IDs) of the child records to use as the new members of this collection',
466-
required: true,
467-
content: {
468-
'application/json': {
469-
schema: {
470-
type: 'array',
471-
items: {
472-
oneOf: generateModelAssociationFKAttributeSchemas(route.model!, models),
473-
}
474-
} as OpenApi.UpdatedSchema,
565+
if(route.actionType === 'shortcutBlueprint') {
566+
generateModelAssociationFKAttributeParameters(route.model!, route.associationAliases, models).map(p => {
567+
if(!route.associationAliases || route.associationAliases.indexOf(p.name) < 0) return;
568+
if (isParam('query', p.name)) return;
569+
pathEntry.parameters.push(p);
570+
});
571+
} else {
572+
if (pathEntry.requestBody) return;
573+
pathEntry.requestBody = {
574+
description: 'The primary key values (usually IDs) of the child records to use as the new members of this collection',
575+
required: true,
576+
content: {
577+
'application/json': {
578+
schema: {
579+
type: 'array',
580+
items: {
581+
oneOf: generateModelAssociationFKAttributeSchemas(route.model!, route.associationAliases, models),
582+
}
583+
} as OpenApi.UpdatedSchema,
584+
},
475585
},
476-
},
477-
};
586+
};
587+
}
478588
},
479589

480590
addShortCutBlueprintRouteNote: () => {
481591
if(route.actionType !== 'shortcutBlueprint') { return; }
482592
pathEntry.summary += ' *';
593+
if(route.action === 'replace') {
594+
pathEntry.description += `\n\nOnly one of the query parameters, that matches the **association** path parameter, should be specified.`;
595+
}
483596
pathEntry.description += `\n\n(\\*) Note that this is a`
484597
+ ` [Sails blueprint shortcut route](https://sailsjs.com/documentation/concepts/blueprints/blueprint-routes#?shortcut-blueprint-routes)`
485598
+ ` (recommended for **development-mode only**)`;
@@ -650,7 +763,7 @@ export const generatePaths = (routes: SwaggerRouteInfo[], templates: BlueprintAc
650763
const dereferenced = components.parameters![ref];
651764
return dereferenced ? dereferenced : parameter;
652765
}
653-
});
766+
}) as OpenApi.Parameter[];
654767

655768
// now add patternVariables that don't already exist
656769
route.variables.map(v => {

lib/transformations.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export const mergeComponents = (dest: OpenApi.Components,
357357
for (const key in source) {
358358
const componentName = key as keyof OpenApi.Components;
359359
if (!dest[componentName]) {
360-
dest[componentName] = {};
360+
(dest[componentName] as unknown) = {};
361361
}
362362
defaults(dest[componentName], source[componentName]);
363363
}

lib/type-formatter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { BlueprintActionTemplates, Modifiers, Action2Response, Defaults } from '
1111
* this is used to map our sails types with the allowed type defintions based on swagger specification
1212
*/
1313
export const swaggerTypes = {
14-
integer: { type: 'integer', format: 'int32', /* comments: 'signed 32 bits' */ },
14+
integer: { type: 'integer', format: 'int64', /* comments: 'signed 64 bits' */ }, // all JavaScript numbers 64-bit
1515
long: { type: 'integer', format: 'int64', /* comments: 'signed 64 bits' */ },
1616
float: { type: 'number', format: 'float' },
1717
double: { type: 'number', format: 'double' },
@@ -188,7 +188,7 @@ export const blueprintActionTemplates: BlueprintActionTemplates = {
188188
},
189189
replace: {
190190
summary: 'Replace for {globalId}',
191-
description: 'Replace all of the foreign **{globalId}** records in one of this record\'s collections.',
191+
description: 'Replace all of the child records in one of this **{globalId}** record\'s associations.',
192192
externalDocs: {
193193
url: 'https://sailsjs.com/documentation/reference/blueprint-api/replace',
194194
description: 'See https://sailsjs.com/documentation/reference/blueprint-api/replace'

lib/utils/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BluePrintAction } from '../interfaces';
22
import swaggerJSDoc from 'swagger-jsdoc';
33
import { OpenApi } from '../../types/openapi';
4+
import { get } from 'lodash';
5+
import { Reference } from 'swagger-schema-official';
46

57
export const blueprintActions: Array<BluePrintAction> = ['findone', 'find', 'create', 'update', 'destroy', 'populate', 'add', 'remove', 'replace'];
68

@@ -60,3 +62,14 @@ export const getUniqueTagsFromPath = (paths: OpenApi.Paths) => {
6062
}
6163
return referencedTags
6264
}
65+
66+
export const resolveRef = (specification: OpenApi.OpenApi, obj: (Reference | unknown)): unknown => {
67+
68+
const path = (obj as Reference).$ref;
69+
if(typeof(path) === 'string' && path.startsWith('#/')) {
70+
const pathElements = path.substring(2).split('/');
71+
return get(specification, pathElements);
72+
}
73+
return obj;
74+
75+
}

0 commit comments

Comments
 (0)