Skip to content

Commit 8ff6a78

Browse files
authored
fix(schema-analyser): handle references correctly when they can not be found (#830)
1 parent 5805fa7 commit 8ff6a78

File tree

3 files changed

+356
-282
lines changed

3 files changed

+356
-282
lines changed

src/adapters/mongoose.js

Lines changed: 3 additions & 281 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
const _ = require('lodash');
2-
const Interface = require('forest-express');
31
const utils = require('../utils/schema');
2+
const Analyser = require('../utils/field-analyser');
43

54
/* eslint-disable */
65
function unflatten(data) {
@@ -25,288 +24,11 @@ function unflatten(data) {
2524
module.exports = async (model, opts) => {
2625
const fields = [];
2726
const paths = unflatten(model.schema.paths);
28-
const { Mongoose } = opts;
29-
// NOTICE: mongoose.base is used when opts.mongoose is not the default connection.
30-
let schemaType;
31-
32-
function formatRef(ref) {
33-
const referenceModel = utils.getReferenceModel(opts, ref);
34-
if (referenceModel) {
35-
return utils.getModelName(referenceModel);
36-
}
37-
Interface.logger.warn(`Cannot find the reference "${ref}" on the model "${model.modelName}".`);
38-
return null;
39-
}
40-
41-
function detectReference(fieldInfo) {
42-
if (fieldInfo.options) {
43-
if (fieldInfo.options.ref && fieldInfo.options.type) {
44-
return `${formatRef(fieldInfo.options.ref)}._id`;
45-
}
46-
if (_.isArray(fieldInfo.options.type) && fieldInfo.options.type.length
47-
&& fieldInfo.options.type[0].ref && fieldInfo.options.type[0].type) {
48-
return `${formatRef(fieldInfo.options.type[0].ref)}._id`;
49-
}
50-
}
51-
return null;
52-
}
53-
54-
function objectType(fieldsInfo, getType) {
55-
const type = { fields: [] };
56-
57-
Object.keys(fieldsInfo).forEach((fieldName) => {
58-
const fieldInfo = fieldsInfo[fieldName];
59-
const field = {
60-
field: fieldName,
61-
type: getType(fieldName),
62-
};
63-
64-
if (fieldName === '_id') {
65-
field.isPrimaryKey = true;
66-
}
67-
68-
if (!field.type) { return; }
69-
70-
const ref = detectReference(fieldInfo);
71-
if (ref) { field.reference = ref; }
72-
73-
if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
74-
field.enums = fieldInfo.enumValues;
75-
}
76-
77-
if (fieldInfo.enum
78-
&& Array.isArray(fieldInfo.enum)
79-
&& fieldInfo.enum.length) {
80-
field.enums = fieldInfo.enum;
81-
}
82-
83-
type.fields.push(field);
84-
});
85-
86-
return type;
87-
}
88-
89-
function getTypeFromNative(type) {
90-
if (type instanceof Array) {
91-
if (_.isEmpty(type)) {
92-
return [null];
93-
}
94-
return [getTypeFromNative(type[0].type || type[0])];
95-
}
96-
if (_.isPlainObject(type)) {
97-
if (_.isEmpty(type)) { return 'Json'; }
98-
99-
if (type.type) {
100-
if (type.enum) {
101-
// NOTICE: Detect enum values for Enums in subdocument arrays.
102-
return 'Enum';
103-
}
104-
return getTypeFromNative(type.type);
105-
}
106-
return objectType(type, (key) => getTypeFromNative(type[key]));
107-
}
108-
if (_.isFunction(type) && type.name === 'ObjectId') {
109-
return 'String';
110-
}
111-
if (type instanceof Mongoose.Schema) {
112-
return schemaType(type);
113-
}
114-
115-
switch (type) {
116-
case String:
117-
return 'String';
118-
case Boolean:
119-
return 'Boolean';
120-
case Number:
121-
return 'Number';
122-
case Date:
123-
return 'Date';
124-
default:
125-
return null;
126-
}
127-
}
128-
129-
function getTypeFromMongoose(fieldInfo) {
130-
if (_.isPlainObject(fieldInfo) && !fieldInfo.path) {
131-
// Deal with Object
132-
return objectType(fieldInfo, (fieldName) => getTypeFromMongoose(fieldInfo[fieldName]));
133-
}
134-
if (fieldInfo.instance === 'Array') {
135-
if (_.isEmpty(fieldInfo.options.type) && !_.isUndefined(fieldInfo.options.type)) {
136-
return 'Json';
137-
}
138-
139-
// Deal with Array
140-
if (fieldInfo.caster.instance && (fieldInfo.caster.options.ref
141-
|| _.keys(fieldInfo.caster.options).length === 0)) {
142-
return [getTypeFromMongoose(fieldInfo.caster)];
143-
}
144-
if (fieldInfo.options.type[0] instanceof Mongoose.Schema) {
145-
// Schema
146-
return [schemaType(fieldInfo.options.type[0])];
147-
}
148-
149-
// NOTICE: Object with `type` reserved keyword.
150-
// See: https://mongoosejs.com/docs/schematypes.html#type-key
151-
if (fieldInfo.options.type[0] instanceof Object
152-
&& fieldInfo.options.type[0].type
153-
// NOTICE: Bypass for schemas like `[{ type: {type: String}, ... }]` where "type" is used
154-
// as property, and thus we are in the case of an array of embedded documents.
155-
// See: https://mongoosejs.com/docs/faq.html#type-key
156-
&& !fieldInfo.options.type[0].type.type) {
157-
return [getTypeFromNative(fieldInfo.options.type[0])];
158-
}
159-
160-
// Object
161-
return [objectType(fieldInfo.options.type[0], (key) =>
162-
getTypeFromNative(fieldInfo.options.type[0][key]))];
163-
}
164-
if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
165-
return 'Enum';
166-
}
167-
if (fieldInfo.instance === 'ObjectID') {
168-
// Deal with ObjectID
169-
return 'String';
170-
}
171-
if (fieldInfo.instance === 'Embedded') {
172-
return objectType(fieldInfo.schema.obj, (fieldName) =>
173-
getTypeFromNative(fieldInfo.schema.obj[fieldName]));
174-
}
175-
if (fieldInfo.instance === 'Mixed') {
176-
// Deal with Mixed object
177-
178-
// NOTICE: Object and {} are detected as Json type as they don't have schema.
179-
if (_.isEmpty(fieldInfo.options.type) && !_.isUndefined(fieldInfo.options.type)) {
180-
return 'Json';
181-
}
182-
if (_.isEmpty(fieldInfo.options) && !_.isUndefined(fieldInfo.options)) {
183-
return 'Json';
184-
}
185-
186-
return null;
187-
}
188-
// Deal with primitive type
189-
return fieldInfo.instance
190-
|| (fieldInfo.options && getTypeFromNative(fieldInfo.options.type))
191-
|| null;
192-
}
193-
194-
schemaType = (type) => ({
195-
fields: _.map(type.paths, (fieldType, fieldName) => {
196-
const field = {
197-
field: fieldName,
198-
type: getTypeFromMongoose(fieldType),
199-
};
200-
201-
if (fieldName === '_id') {
202-
field.isPrimaryKey = true;
203-
}
204-
205-
if (fieldType.enumValues && fieldType.enumValues.length) {
206-
field.enums = fieldType.enumValues;
207-
}
208-
209-
return field;
210-
}),
211-
});
212-
213-
function getRequired(fieldInfo) {
214-
return fieldInfo.isRequired === true
215-
|| (
216-
fieldInfo.path === '_id'
217-
&& !fieldInfo.options.auto
218-
&& fieldInfo.options.type !== Mongoose.ObjectId
219-
);
220-
}
221-
222-
function getValidations(fieldInfo) {
223-
const validations = [];
224-
225-
if (fieldInfo.validators && fieldInfo.validators.length > 0) {
226-
_.each(fieldInfo.validators, (validator) => {
227-
if (validator.type === 'required') {
228-
validations.push({
229-
type: 'is present',
230-
});
231-
}
232-
233-
if (validator.type === 'minlength') {
234-
validations.push({
235-
type: 'is longer than',
236-
value: validator.minlength,
237-
});
238-
}
239-
240-
if (validator.type === 'maxlength') {
241-
validations.push({
242-
type: 'is shorter than',
243-
value: validator.maxlength,
244-
});
245-
}
246-
247-
if (validator.type === 'min') {
248-
validations.push({
249-
type: 'is greater than',
250-
value: validator.min,
251-
});
252-
}
253-
254-
if (validator.type === 'max') {
255-
validations.push({
256-
type: 'is less than',
257-
value: validator.max,
258-
});
259-
}
260-
});
261-
}
262-
263-
return validations;
264-
}
265-
266-
function getFieldSchema(path) {
267-
const fieldInfo = paths[path];
268-
269-
const schema = { field: path, type: getTypeFromMongoose(fieldInfo) };
270-
271-
const ref = detectReference(fieldInfo);
272-
if (ref) { schema.reference = ref; }
273-
274-
if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
275-
schema.enums = fieldInfo.enumValues;
276-
}
277-
278-
// NOTICE: Create enums from caster (for ['Enum'] type).
279-
if (fieldInfo.caster && fieldInfo.caster.enumValues && fieldInfo.caster.enumValues.length) {
280-
schema.enums = fieldInfo.caster.enumValues;
281-
}
282-
283-
const isRequired = getRequired(fieldInfo);
284-
if (isRequired) {
285-
schema.isRequired = isRequired;
286-
}
287-
288-
if (path === '_id') {
289-
schema.isPrimaryKey = true;
290-
}
291-
292-
if (fieldInfo.options && !_.isNull(fieldInfo.options.default)
293-
&& !_.isUndefined(fieldInfo.options.default)
294-
&& !_.isFunction(fieldInfo.options.default)) {
295-
schema.defaultValue = fieldInfo.options.default;
296-
}
297-
298-
schema.validations = getValidations(fieldInfo);
299-
300-
if (schema.validations.length === 0) {
301-
delete schema.validations;
302-
}
303-
304-
return schema;
305-
}
27+
const analyser = new Analyser(model, opts);
30628

30729
Object.keys(paths).forEach(async (path) => {
30830
if (path === '__v') { return; }
309-
const field = getFieldSchema(path);
31+
const field = analyser.getFieldSchema(path, paths[path]);
31032
fields.push(field);
31133
});
31234

0 commit comments

Comments
 (0)