Skip to content

Commit f7081c1

Browse files
authored
feat(flattener): native flattened fields are now entirely introspected (#833)
1 parent 93d35b1 commit f7081c1

File tree

6 files changed

+165
-22
lines changed

6 files changed

+165
-22
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"dependencies": {
2828
"@babel/runtime": "7.15.4",
2929
"bluebird": "2.9.25",
30-
"forest-express": "9.4.7",
30+
"forest-express": "9.5.0",
3131
"http-errors": "1.7.2",
3232
"lodash": "4.17.21",
3333
"moment": "2.29.2",

src/services/flattener.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import _ from 'lodash';
22
import Interface from 'forest-express';
3+
import FieldIntrospector from '../utils/field-analyser';
4+
import { getMongooseSchemaFromFieldPath } from '../utils/schema';
35

46
const FLATTEN_SEPARATOR = '@@@';
57

68
module.exports = class Flattener {
7-
constructor(schema, flatten) {
9+
constructor(schema, flatten, model, lianaOptions) {
810
this.schema = schema;
911
this.flatten = flatten;
12+
this.model = model;
13+
this.lianaOptions = lianaOptions;
1014
}
1115

1216
_removeWrongFlattenConfiguration(index) {
@@ -202,7 +206,20 @@ module.exports = class Flattener {
202206
});
203207
} else {
204208
schema.field = parentFieldName;
205-
newFields.push(schema);
209+
210+
const fieldInfo = getMongooseSchemaFromFieldPath(
211+
schema.field.split(FLATTEN_SEPARATOR).join('.'),
212+
this.model,
213+
);
214+
215+
if (typeof schema.type === 'string' && fieldInfo) {
216+
const introspectedSchema = new FieldIntrospector(this.model, this.lianaOptions)
217+
.getFieldSchema(schema.field, fieldInfo);
218+
219+
newFields.push(introspectedSchema);
220+
} else {
221+
newFields.push(schema);
222+
}
206223
}
207224
}
208225

src/utils/schema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ const getNestedFieldType = (mongooseSchema, nestedFieldPath) => {
3939
};
4040

4141
exports.getNestedFieldType = getNestedFieldType;
42+
43+
exports.getMongooseSchemaFromFieldPath = (fieldPath, model) => model.schema.paths[fieldPath]
44+
|| model.schema.singleNestedPaths[fieldPath] || null;

test/tests/services/flattener.test.js

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import Interface from 'forest-express';
2+
import mongoose from 'mongoose';
23
import Flattener from '../../../src/services/flattener';
4+
import mongooseConnect from '../../utils/mongoose-connect';
35

46
const FLATTEN_SEPARATOR = '@@@';
57

68
describe('service > Flattener', () => {
79
let errorLoggerSpy;
810
let warnLoggerSpy;
911

12+
const companiesSchema = mongoose.Schema({
13+
name: String,
14+
});
15+
const carsSchema = mongoose.Schema({
16+
name: String,
17+
engine: {
18+
horsePower: { type: String, default: '110cv', enum: ['110cv', '115cv', '130cv'] },
19+
identification: {
20+
manufacturer: { type: mongoose.Schema.Types.ObjectId, ref: 'companies' },
21+
},
22+
owner: { type: mongoose.Schema.Types.ObjectId, ref: 'companies' },
23+
partners: [{ type: mongoose.Schema.Types.ObjectId, ref: 'companies' }],
24+
},
25+
});
26+
let carsModel;
1027
const generateFlattenedEngineSchema = () => ({
1128
name: 'cars',
1229
fields: [{
@@ -110,15 +127,30 @@ describe('service > Flattener', () => {
110127
type: 'String',
111128
}],
112129
});
130+
const lianaOptions = {
131+
connections: {},
132+
mongoose,
133+
};
113134

114-
beforeAll(() => {
135+
beforeAll(async () => {
115136
Interface.Schemas.schemas.cars = {
116137
fields: [],
117138
};
118139
errorLoggerSpy = jest
119140
.spyOn(Interface.logger, 'error');
120141
warnLoggerSpy = jest
121142
.spyOn(Interface.logger, 'warn');
143+
144+
await mongooseConnect();
145+
146+
lianaOptions.connections.default = mongoose.connection;
147+
mongoose.model('cars', carsSchema);
148+
mongoose.model('companies', companiesSchema);
149+
carsModel = lianaOptions.connections.default.models.cars;
150+
});
151+
152+
afterAll(async () => {
153+
await mongoose.connection.close();
122154
});
123155

124156
describe('unflattenParams', () => {
@@ -163,7 +195,7 @@ describe('service > Flattener', () => {
163195
expect.assertions(2);
164196

165197
jest.resetAllMocks();
166-
const fieldsFlattener = new Flattener({ name: 'cars' }, { });
198+
const fieldsFlattener = new Flattener({ name: 'cars' }, { }, carsModel, lianaOptions);
167199
fieldsFlattener.validateOptions();
168200

169201
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
@@ -174,7 +206,7 @@ describe('service > Flattener', () => {
174206
expect.assertions(1);
175207

176208
jest.resetAllMocks();
177-
const fieldsFlattener = new Flattener({ name: 'cars' }, { });
209+
const fieldsFlattener = new Flattener({ name: 'cars' }, { }, carsModel, lianaOptions);
178210

179211
expect(() => { fieldsFlattener.validateOptions(); }).not.toThrow();
180212
});
@@ -187,7 +219,7 @@ describe('service > Flattener', () => {
187219

188220
beforeAll(() => {
189221
jest.resetAllMocks();
190-
fieldsFlattener = new Flattener({ fields: [] }, ['test']);
222+
fieldsFlattener = new Flattener({ fields: [] }, ['test'], carsModel, lianaOptions);
191223
fieldsFlattener.validateOptions();
192224
});
193225

@@ -210,7 +242,7 @@ describe('service > Flattener', () => {
210242

211243
beforeAll(() => {
212244
jest.resetAllMocks();
213-
fieldsFlattener = new Flattener({ fields: [] }, [{ field: 'test' }]);
245+
fieldsFlattener = new Flattener({ fields: [] }, [{ field: 'test' }], carsModel, lianaOptions);
214246
fieldsFlattener.validateOptions();
215247
});
216248

@@ -234,7 +266,7 @@ describe('service > Flattener', () => {
234266

235267
beforeAll(() => {
236268
jest.resetAllMocks();
237-
fieldsFlattener = new Flattener({ fields: [] }, [{ }]);
269+
fieldsFlattener = new Flattener({ fields: [] }, [{ }], carsModel, lianaOptions);
238270
fieldsFlattener.validateOptions();
239271
});
240272

@@ -258,7 +290,12 @@ describe('service > Flattener', () => {
258290
expect.assertions(2);
259291

260292
jest.resetAllMocks();
261-
const fieldsFlattener = new Flattener(generateDefaultEngineSchema(), [{ field: 'engine', level: null }]);
293+
const fieldsFlattener = new Flattener(
294+
generateDefaultEngineSchema(),
295+
[{ field: 'engine', level: null }],
296+
carsModel,
297+
lianaOptions,
298+
);
262299
fieldsFlattener.validateOptions();
263300

264301
expect(warnLoggerSpy).toHaveBeenCalledTimes(1);
@@ -270,7 +307,12 @@ describe('service > Flattener', () => {
270307

271308
jest.resetAllMocks();
272309
const flatten = [{ field: 'engine', level: null }];
273-
const fieldsFlattener = new Flattener(generateDefaultEngineSchema(), flatten);
310+
const fieldsFlattener = new Flattener(
311+
generateDefaultEngineSchema(),
312+
flatten,
313+
carsModel,
314+
lianaOptions,
315+
);
274316
fieldsFlattener.validateOptions();
275317

276318
expect(flatten[0].level).not.toBeDefined();
@@ -279,12 +321,27 @@ describe('service > Flattener', () => {
279321
});
280322

281323
describe('flattening a field', () => {
324+
it('should deeply introspect mongoose schema for native fields', () => {
325+
expect.assertions(2);
326+
327+
const schema = generateDefaultEngineSchema();
328+
329+
const fieldsFlattener = new Flattener(schema, ['engine'], carsModel, lianaOptions);
330+
fieldsFlattener.flattenFields();
331+
332+
const horsePowerField = schema.fields
333+
.find((field) => field.field === 'engine@@@horsePower');
334+
335+
expect(horsePowerField.defaultValue).toStrictEqual('110cv');
336+
expect(horsePowerField.enums).toStrictEqual(['110cv', '115cv', '130cv']);
337+
});
338+
282339
it(`should merge fields name with ${FLATTEN_SEPARATOR} as separator`, () => {
283340
expect.assertions(1);
284341

285342
const schema = generateDefaultEngineSchema();
286343

287-
const fieldsFlattener = new Flattener(schema, ['engine']);
344+
const fieldsFlattener = new Flattener(schema, ['engine'], carsModel, lianaOptions);
288345
fieldsFlattener.flattenFields();
289346

290347
expect(schema.fields).toContainEqual({
@@ -301,7 +358,7 @@ describe('service > Flattener', () => {
301358

302359
const schema = generateDefaultEngineSchema();
303360

304-
const fieldsFlattener = new Flattener(schema, ['engine']);
361+
const fieldsFlattener = new Flattener(schema, ['engine'], carsModel, lianaOptions);
305362
fieldsFlattener.flattenFields();
306363

307364
expect(schema.fields).toHaveLength(5);
@@ -315,7 +372,7 @@ describe('service > Flattener', () => {
315372
expect.assertions(1);
316373

317374
const schema = generateDefaultEngineSchema();
318-
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 100 }]);
375+
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 100 }], carsModel, lianaOptions);
319376

320377
fieldsFlattener.flattenFields();
321378

@@ -327,7 +384,7 @@ describe('service > Flattener', () => {
327384
expect.assertions(1);
328385

329386
const schema = generateDefaultEngineSchema();
330-
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 0 }]);
387+
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 0 }], carsModel, lianaOptions);
331388

332389
fieldsFlattener.flattenFields();
333390

@@ -340,7 +397,7 @@ describe('service > Flattener', () => {
340397
expect.assertions(1);
341398

342399
const schema = generateDefaultEngineSchema();
343-
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 0 }]);
400+
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: 0 }], carsModel, lianaOptions);
344401

345402
fieldsFlattener.flattenFields();
346403

@@ -352,7 +409,7 @@ describe('service > Flattener', () => {
352409
expect.assertions(1);
353410

354411
const schema = generateDefaultEngineSchema();
355-
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: -2 }]);
412+
const fieldsFlattener = new Flattener(schema, [{ field: 'engine', level: -2 }], carsModel, lianaOptions);
356413

357414
fieldsFlattener.flattenFields();
358415

test/tests/utils/schema.test.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import mongoose from 'mongoose';
22
import mongooseConnect from '../../utils/mongoose-connect';
3-
import { getNestedFieldType } from '../../../src/utils/schema';
3+
import { getNestedFieldType, getMongooseSchemaFromFieldPath } from '../../../src/utils/schema';
44

55
describe('schema', () => {
66
let nestedModelSchema;
@@ -127,4 +127,70 @@ describe('schema', () => {
127127
).toBeUndefined();
128128
});
129129
});
130+
131+
describe('getMongooseSchemaFromFieldPath', () => {
132+
let samplesModel;
133+
134+
beforeAll(() => {
135+
const schema = new mongoose.Schema({
136+
subDocument: new mongoose.Schema({
137+
attribute1: String,
138+
attribute2: Number,
139+
nestedPath: {
140+
attribute3: String,
141+
},
142+
}),
143+
nestedPath: {
144+
attribute1: String,
145+
attribute2: Number,
146+
},
147+
});
148+
samplesModel = mongoose.model('samples', schema);
149+
});
150+
151+
describe('when the field does not exist in schema', () => {
152+
it('should return null', () => {
153+
expect.assertions(1);
154+
155+
const result = getMongooseSchemaFromFieldPath('subDocument.notExisting', samplesModel);
156+
157+
expect(result).toBeNull();
158+
});
159+
});
160+
161+
describe('when the field exists in schema', () => {
162+
describe('when the field is from a subDocument (subSchema)', () => {
163+
it('should return the field info', () => {
164+
expect.assertions(1);
165+
166+
const fieldName = 'subDocument.attribute1';
167+
const fieldInfo = getMongooseSchemaFromFieldPath(fieldName, samplesModel);
168+
169+
expect(fieldInfo).toBeDefined();
170+
});
171+
});
172+
173+
describe('when the field is from a nestPath (nestedObject)', () => {
174+
it('should return the field info', () => {
175+
expect.assertions(1);
176+
177+
const fieldName = 'nestedPath.attribute1';
178+
const fieldInfo = getMongooseSchemaFromFieldPath(fieldName, samplesModel);
179+
180+
expect(fieldInfo).toBeDefined();
181+
});
182+
});
183+
184+
describe('when the field is from both nestedPath and embeddedDocument', () => {
185+
it('should return the field info', () => {
186+
expect.assertions(1);
187+
188+
const fieldName = 'subDocument.nestedPath.attribute3';
189+
const fieldInfo = getMongooseSchemaFromFieldPath(fieldName, samplesModel);
190+
191+
expect(fieldInfo).toBeDefined();
192+
});
193+
});
194+
});
195+
});
130196
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4405,10 +4405,10 @@ for-in@^1.0.2:
44054405
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
44064406
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
44074407

4408-
forest-express@9.4.7:
4409-
version "9.4.7"
4410-
resolved "https://registry.yarnpkg.com/forest-express/-/forest-express-9.4.7.tgz#0246c563fc27fec3d870bf5e7a6547e5c29c3771"
4411-
integrity sha512-+F9wpuecIVTS1n1pvGSA1Q9ZEyMlEfAZ3bap3wFukYojm3sdecZ1vFU1KzwxkXBlyHh6v/uuHhceBJnP0S/DIA==
4408+
forest-express@9.5.0:
4409+
version "9.5.0"
4410+
resolved "https://registry.yarnpkg.com/forest-express/-/forest-express-9.5.0.tgz#cd6b3bb958b4a437b75ad1fd6957d69743a526b1"
4411+
integrity sha512-8YkxAXha0V+C/f7+1V9QQfu6kUYYjBDAM/e/EtFtTjCll0fpSKPeUKMMEG0yVATYdIfHRjlSxs2NPjgCuH51IA==
44124412
dependencies:
44134413
"@babel/runtime" "7.10.1"
44144414
"@forestadmin/context" "1.31.0"

0 commit comments

Comments
 (0)