diff --git a/packages/_example/src/forest/customizations/rental.ts b/packages/_example/src/forest/customizations/rental.ts index ee0828b370..8af34c762b 100644 --- a/packages/_example/src/forest/customizations/rental.ts +++ b/packages/_example/src/forest/customizations/rental.ts @@ -8,10 +8,8 @@ export default (collection: Collection) => dependencies: ['startDate', 'endDate'], getValues: records => records.map(record => { - // Datasource is sending dates, typing is expecting strings - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const timeDifference = record.endDate.getTime() - record.startDate.getTime(); + const timeDifference = + new Date(record.endDate).getTime() - new Date(record.startDate).getTime(); return Math.trunc(timeDifference / (1000 * 60 * 60 * 24)); }), diff --git a/packages/datasource-sequelize/src/collection.ts b/packages/datasource-sequelize/src/collection.ts index 64f9af29fd..e5ba624223 100644 --- a/packages/datasource-sequelize/src/collection.ts +++ b/packages/datasource-sequelize/src/collection.ts @@ -16,6 +16,7 @@ import { import AggregationUtils from './utils/aggregation'; import ModelConverter from './utils/model-to-collection-schema-converter'; import QueryConverter from './utils/query-converter'; +import Serializer from './utils/serializer'; export default class SequelizeCollection extends BaseCollection { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -48,7 +49,7 @@ export default class SequelizeCollection extends BaseCollection { async create(caller: Caller, data: RecordData[]): Promise { const records = await this.model.bulkCreate(data); - return records.map(record => record.get({ plain: true })); + return records.map(record => Serializer.serialize(record.get({ plain: true }))); } async list( @@ -82,7 +83,7 @@ export default class SequelizeCollection extends BaseCollection { const records = await this.model.findAll(query); - return records.map(record => record.get({ plain: true })); + return records.map(record => Serializer.serialize(record.get({ plain: true }))); } async update(caller: Caller, filter: Filter, patch: RecordData): Promise { diff --git a/packages/datasource-sequelize/src/utils/aggregation.ts b/packages/datasource-sequelize/src/utils/aggregation.ts index 639ebd119d..3dff395302 100644 --- a/packages/datasource-sequelize/src/utils/aggregation.ts +++ b/packages/datasource-sequelize/src/utils/aggregation.ts @@ -11,6 +11,7 @@ import { import { Fn } from 'sequelize/types/utils'; import DateAggregationConverter from './date-aggregation-converter'; +import Serializer from './serializer'; export default class AggregationUtils { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -125,7 +126,9 @@ export default class AggregationUtils { }; aggregationQueryGroup?.forEach(({ field }) => { - aggregateResult.group[field] = aggregate[this.getGroupFieldName(field)]; + aggregateResult.group[field] = Serializer.serializeValue( + aggregate[this.getGroupFieldName(field)], + ); }); return aggregateResult; diff --git a/packages/datasource-sequelize/src/utils/serializer.ts b/packages/datasource-sequelize/src/utils/serializer.ts new file mode 100644 index 0000000000..ba869d05fc --- /dev/null +++ b/packages/datasource-sequelize/src/utils/serializer.ts @@ -0,0 +1,26 @@ +import { RecordData } from '@forestadmin/datasource-toolkit'; + +export default class Serializer { + static serialize(record: RecordData): RecordData { + Object.entries(record).forEach(([name, value]) => { + if (value instanceof Date) record[name] = this.serializeValue(value); + if (Array.isArray(value)) this.serializeValue(value); // the change is by references + if (value instanceof Object) return this.serialize(record[name]); + }); + + return record; + } + + static serializeValue(value: unknown): unknown { + if (value instanceof Date) return value.toISOString(); + + if (Array.isArray(value)) { + value.forEach((v, i) => { + // serialize by reference to improve performances by avoiding the copies + if (value instanceof Date) value[i] = v.toISOString(); + }); + } + + return value; + } +} diff --git a/packages/datasource-sequelize/test/collection.test.ts b/packages/datasource-sequelize/test/collection.test.ts index 836dd5c048..dcd8ab1deb 100644 --- a/packages/datasource-sequelize/test/collection.test.ts +++ b/packages/datasource-sequelize/test/collection.test.ts @@ -46,14 +46,11 @@ describe('SequelizeDataSource > Collection', () => { }); describe('create', () => { - const setup = () => { + const setup = (recordData: RecordData[]) => { const { dataSource, name, sequelize } = makeConstructorParams(); const sequelizeCollection = new SequelizeCollection(name, dataSource, sequelize.models[name]); - const recordData = Symbol('recordData'); - const record = { - get: jest.fn(() => recordData), - }; - const bulkCreate = jest.fn().mockResolvedValue([record]); + const records = recordData.map(r => ({ get: jest.fn().mockReturnValueOnce(r) })); + const bulkCreate = jest.fn().mockResolvedValue(records); // eslint-disable-next-line @typescript-eslint/dot-notation sequelizeCollection['model'] = { ...sequelize.models[name], @@ -62,24 +59,53 @@ describe('SequelizeDataSource > Collection', () => { return { bulkCreate, - recordData, sequelizeCollection, }; }; it('should delegate work to `sequelize.model.bulkCreate`', async () => { - const { bulkCreate, recordData, sequelizeCollection } = setup(); - const data = Symbol('data') as unknown as RecordData[]; + const data = [{ data: 'data' }, { data: 10 }, { data: ['Enum'] }]; + const { bulkCreate, sequelizeCollection } = setup(data); + + await expect(sequelizeCollection.create(factories.caller.build(), data)).resolves.toEqual( + data, + ); + expect(bulkCreate).toHaveBeenCalledWith(data); + }); + + it('should serialize date as iso string', async () => { + const data = [{ date: new Date('2000-01-02') }]; + const { bulkCreate, sequelizeCollection } = setup(data); await expect(sequelizeCollection.create(factories.caller.build(), data)).resolves.toEqual([ - recordData, + { date: '2000-01-02T00:00:00.000Z' }, + ]); + expect(bulkCreate).toHaveBeenCalledWith(data); + }); + + it('should serialize date as iso string with many to one relation', async () => { + const data = [{ manyToOne: { date: new Date('2000-01-02') } }]; + const { bulkCreate, sequelizeCollection } = setup(data); + + await expect(sequelizeCollection.create(factories.caller.build(), data)).resolves.toEqual([ + { manyToOne: { date: '2000-01-02T00:00:00.000Z' } }, + ]); + expect(bulkCreate).toHaveBeenCalledWith(data); + }); + + it('should serialize array of date as iso string', async () => { + const data = [{ dates: [new Date('2000-01-02')] }]; + const { bulkCreate, sequelizeCollection } = setup(data); + + await expect(sequelizeCollection.create(factories.caller.build(), data)).resolves.toEqual([ + { dates: ['2000-01-02T00:00:00.000Z'] }, ]); expect(bulkCreate).toHaveBeenCalledWith(data); }); }); describe('list', () => { - const setup = () => { + const setup = (recordData: RecordData[]) => { const { dataSource, name, sequelize } = makeConstructorParams(); const relation = sequelize.define('relation', { @@ -89,11 +115,8 @@ describe('SequelizeDataSource > Collection', () => { const model = sequelize.model(name); model.belongsTo(relation); - const recordData = Symbol('recordData'); - const record = { - get: jest.fn(() => recordData), - }; - const findAll = jest.fn().mockResolvedValue([record]); + const records = recordData.map(r => ({ get: jest.fn().mockReturnValueOnce(r) })); + const findAll = jest.fn().mockResolvedValue(records); model.findAll = findAll; @@ -101,21 +124,22 @@ describe('SequelizeDataSource > Collection', () => { return { findAll, - record, recordData, sequelizeCollection, + records, }; }; it('should delegate work to `sequelize.model.findAll`', async () => { - const { findAll, recordData, sequelizeCollection } = setup(); + const recordData = [{ data: 'data' }]; + const { findAll, sequelizeCollection } = setup(recordData); const filter = new Filter({}); const projection = new Projection(); const result = await sequelizeCollection.list(factories.caller.build(), filter, projection); expect(result).toBeArrayOfSize(1); - expect(result[0]).toBe(recordData); + expect(result[0]).toBe(recordData[0]); expect(findAll).toHaveBeenCalledWith( expect.objectContaining({ attributes: projection, @@ -124,19 +148,35 @@ describe('SequelizeDataSource > Collection', () => { }); it('should resolve with plain records', async () => { - const { record, recordData, sequelizeCollection } = setup(); + const recordData = [{ data: 'data' }]; + const { sequelizeCollection, records } = setup(recordData); const filter = new Filter({}); const projection = new Projection(); const result = await sequelizeCollection.list(factories.caller.build(), filter, projection); expect(result).toBeArrayOfSize(1); - expect(result[0]).toBe(recordData); - expect(record.get).toHaveBeenCalledWith({ plain: true }); + expect(result[0]).toBe(recordData[0]); + expect(records[0].get).toHaveBeenCalledWith({ plain: true }); + }); + + it('should serialize date as iso string', async () => { + const recordData = [{ data: new Date('2000-10-01') }, { data: new Date('2000-10-02') }]; + const { sequelizeCollection } = setup(recordData); + const filter = new Filter({}); + const projection = new Projection(); + + const result = await sequelizeCollection.list(factories.caller.build(), filter, projection); + + expect(result).toEqual([ + { data: '2000-10-01T00:00:00.000Z' }, + { data: '2000-10-02T00:00:00.000Z' }, + ]); }); it('should add include from condition tree, sort and projection', async () => { - const { findAll, sequelizeCollection } = setup(); + const recordData = [{ data: 'data' }]; + const { findAll, sequelizeCollection } = setup(recordData); const filter = new PaginatedFilter({ sort: new Sort({ field: 'relation1:field1', ascending: true }), conditionTree: new ConditionTreeLeaf('relation:aField', 'Equal', 42), @@ -274,7 +314,7 @@ describe('SequelizeDataSource > Collection', () => { renamed__field____grouped__: 'renamed__field__:value', 'relations:as__field____grouped__': 'relations:as__field__:value', 'relations:renamed__as__field____grouped__': 'relations:renamed__as__field__:value', - date__field____grouped__: 'date__field__:value', + date__field____grouped__: new Date('2000-10-01'), }, ]); @@ -292,7 +332,7 @@ describe('SequelizeDataSource > Collection', () => { }; }; - describe('whitout aggregate field', () => { + describe('without aggregate field', () => { it('should aggregate on *', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ @@ -356,7 +396,7 @@ describe('SequelizeDataSource > Collection', () => { }); }); - describe('when field name is deferent as column', () => { + describe('when field name is different as column', () => { it('should aggregate properly', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ @@ -451,7 +491,7 @@ describe('SequelizeDataSource > Collection', () => { }); }); - describe('when field name is deferent as column', () => { + describe('when field name is different as column', () => { it('should aggregate properly', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ @@ -531,7 +571,7 @@ describe('SequelizeDataSource > Collection', () => { ); }); - describe('when field name is deferent as column', () => { + describe('when field name is different as column', () => { it('should compute group properly', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ @@ -622,7 +662,7 @@ describe('SequelizeDataSource > Collection', () => { ); }); - describe('when field name is deferent as column', () => { + describe('when field name is different as column', () => { it('should aggregate properly', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ @@ -698,7 +738,8 @@ describe('SequelizeDataSource > Collection', () => { await expect( sequelizeCollection.aggregate(factories.caller.build(), filter, aggregation), ).resolves.toEqual([ - { group: { date__field__: 'date__field__:value' }, value: '__aggregate__:value' }, + // date should be serialize in iso string + { group: { date__field__: '2000-10-01T00:00:00.000Z' }, value: '__aggregate__:value' }, ]); expect(findAll).toHaveBeenCalledTimes(1); @@ -722,7 +763,7 @@ describe('SequelizeDataSource > Collection', () => { }); describe('when dialect is mssql', () => { - it('should add date agregation to group', async () => { + it('should add date aggregation to group', async () => { const { findAll, sequelizeCollection } = setup('mssql'); const aggregation = new Aggregation({ @@ -734,7 +775,11 @@ describe('SequelizeDataSource > Collection', () => { await expect( sequelizeCollection.aggregate(factories.caller.build(), filter, aggregation), ).resolves.toEqual([ - { group: { date__field__: 'date__field__:value' }, value: '__aggregate__:value' }, + { + // date should be serialize in iso string + group: { date__field__: '2000-10-01T00:00:00.000Z' }, + value: '__aggregate__:value', + }, ]); const aggregateFunction = { @@ -758,7 +803,7 @@ describe('SequelizeDataSource > Collection', () => { describe('on sort', () => { describe('when dialect is postgres', () => { - it('should sort on aggragate by default', async () => { + it('should sort on aggregate by default', async () => { const { findAll, sequelizeCollection } = setup(); const aggregation = new Aggregation({ operation: 'Count', @@ -779,7 +824,7 @@ describe('SequelizeDataSource > Collection', () => { }); describe('when dialect is mssql', () => { - it('should sort on aggragate by default', async () => { + it('should sort on aggregate by default', async () => { const { findAll, sequelizeCollection } = setup('mssql'); const aggregation = new Aggregation({ operation: 'Count', @@ -799,7 +844,7 @@ describe('SequelizeDataSource > Collection', () => { }); }); - it('should sort on aggragate by default', async () => { + it('should sort on aggregate by default', async () => { const { findAll, sequelizeCollection } = setup('mysql'); const aggregation = new Aggregation({ operation: 'Count',