diff --git a/packages/datasource-mongoose/src/collection.ts b/packages/datasource-mongoose/src/collection.ts index e46243e99f..4d4c69fa32 100644 --- a/packages/datasource-mongoose/src/collection.ts +++ b/packages/datasource-mongoose/src/collection.ts @@ -16,6 +16,7 @@ import { Error, Model, PipelineStage } from 'mongoose'; import MongooseSchema from './mongoose/schema'; import { Stack } from './types'; +import addNullValues from './utils/add-null-values'; import { buildSubdocumentPatch, compareIds, @@ -79,7 +80,7 @@ export default class MongooseCollection extends BaseCollection { ...ProjectionGenerator.project(projection), ]); - return replaceMongoTypes(records); + return addNullValues(replaceMongoTypes(records), projection); } async aggregate( diff --git a/packages/datasource-mongoose/src/utils/add-null-values.ts b/packages/datasource-mongoose/src/utils/add-null-values.ts new file mode 100644 index 0000000000..868387a659 --- /dev/null +++ b/packages/datasource-mongoose/src/utils/add-null-values.ts @@ -0,0 +1,48 @@ +function addNullValuesOnRecord( + record: Record, + projection: string[], +): Record { + if (!record) return record; + + const result = { ...record }; + + for (const field of projection) { + const fieldPrefix = field.split(':')[0]; + + if (result[fieldPrefix] === undefined) { + result[fieldPrefix] = null; + } + } + + const nestedPrefixes = new Set( + projection.filter(field => field.includes(':')).map(field => field.split(':')[0]), + ); + + for (const nestedPrefix of nestedPrefixes) { + const childPaths = projection + .filter(field => field.startsWith(`${nestedPrefix}:`)) + .map(field => field.substring(nestedPrefix.length + 1)); + + if (result[nestedPrefix] !== null && result[nestedPrefix] !== undefined) { + if (Array.isArray(result[nestedPrefix])) { + result[nestedPrefix] = (result[nestedPrefix] as Record[]).map( + childRecord => addNullValuesOnRecord(childRecord, childPaths), + ); + } else if (typeof result[nestedPrefix] === 'object') { + result[nestedPrefix] = addNullValuesOnRecord( + result[nestedPrefix] as Record, + childPaths, + ); + } + } + } + + return result; +} + +export default function addNullValues( + records: Record[], + projection: string[], +): Record[] { + return records.map(record => addNullValuesOnRecord(record, projection)); +} diff --git a/packages/datasource-mongoose/test/integration/many-to-one/collection_list.test.ts b/packages/datasource-mongoose/test/integration/many-to-one/collection_list.test.ts index 83f26dc10d..6becc754f6 100644 --- a/packages/datasource-mongoose/test/integration/many-to-one/collection_list.test.ts +++ b/packages/datasource-mongoose/test/integration/many-to-one/collection_list.test.ts @@ -115,7 +115,7 @@ describe('MongooseCollection', () => { new Projection('name', 'storeId__manyToOne:name'), ); - expect(expectedOwner).toEqual([{ name: 'aOwner' }]); + expect(expectedOwner).toEqual([{ name: 'aOwner', storeId__manyToOne: null }]); }); it('should filter by relation', async () => { diff --git a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts index a589a4f454..39e79949e7 100644 --- a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts +++ b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts @@ -107,6 +107,22 @@ describe('MongooseCollection', () => { expect(records).toEqual([{ message: 'a message' }]); }); + + it('should return the given record with null as the nested projection', async () => { + connection = await setupReview('collection_review_list'); + const dataSource = new MongooseDatasource(connection); + const review = dataSource.getCollection('review'); + const expectedRecord = { message: 'a message', title: 'a title' }; + await review.create(factories.caller.build(), [expectedRecord]); + + const records = await review.list( + factories.caller.build(), + factories.filter.build(), + new Projection('nestedField:nested:level'), + ); + + expect(records).toEqual([{ nestedField: null }]); + }); }); describe('condition tree', () => { @@ -389,7 +405,7 @@ describe('MongooseCollection', () => { field: 'nestedField.nested.level', }), }), - new Projection('nestedField.nested.level'), + new Projection('nestedField:nested:level'), ); expect(records).toEqual([{ nestedField: { nested: [{ level: 10 }] } }]); diff --git a/packages/datasource-mongoose/test/utils/add-null-values.test.ts b/packages/datasource-mongoose/test/utils/add-null-values.test.ts new file mode 100644 index 0000000000..b26cfd70a3 --- /dev/null +++ b/packages/datasource-mongoose/test/utils/add-null-values.test.ts @@ -0,0 +1,68 @@ +import addNullValues from '../../src/utils/add-null-values'; + +describe('addNullValues', () => { + it('should return a different object with null values added', () => { + const input = { + id: 1, + name: 'John', + }; + const projection = ['id', 'name', 'address']; + + const result = addNullValues([input], projection); + + expect(result).not.toBe(input); + expect(result).toEqual([{ id: 1, name: 'John', address: null }]); + }); + + it('should add null values in nested objects', () => { + const input = { + id: 1, + name: 'John', + address: { + street: 'Main Street', + }, + }; + const projection = ['id', 'name', 'address:street', 'address:city']; + + const result = addNullValues([input], projection); + + expect(result).toEqual([ + { + id: 1, + name: 'John', + address: { + street: 'Main Street', + city: null, + }, + }, + ]); + }); + + it('should add null values in nested arrays', () => { + const input = { + id: 1, + name: 'John', + addresses: [ + { + street: 'Main Street', + }, + ], + }; + const projection = ['id', 'name', 'addresses:street', 'addresses:city']; + + const result = addNullValues([input], projection); + + expect(result).toEqual([ + { + id: 1, + name: 'John', + addresses: [ + { + street: 'Main Street', + city: null, + }, + ], + }, + ]); + }); +});