From 104b9d1624438270517da1f2f3d1b4d8d62f1243 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 2 Jun 2022 11:12:04 +0200 Subject: [PATCH 1/3] fix(datasource-sequelize): serialize record to transform date to iso string --- .../src/forest/customizations/rental.ts | 3 +- .../datasource-sequelize/src/collection.ts | 5 +- .../src/utils/aggregation.ts | 5 +- .../src/utils/serializer.ts | 17 ++++ .../test/collection.test.ts | 95 ++++++++++++------- 5 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 packages/datasource-sequelize/src/utils/serializer.ts diff --git a/packages/_example/src/forest/customizations/rental.ts b/packages/_example/src/forest/customizations/rental.ts index ee0828b370..efd77e96c2 100644 --- a/packages/_example/src/forest/customizations/rental.ts +++ b/packages/_example/src/forest/customizations/rental.ts @@ -11,7 +11,8 @@ export default (collection: Collection) => // 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..4d7876fe25 --- /dev/null +++ b/packages/datasource-sequelize/src/utils/serializer.ts @@ -0,0 +1,17 @@ +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); + }); + + return record; + } + + static serializeValue(value: unknown): unknown { + if (value instanceof Date) return value.toISOString(); + + return value; + } +} diff --git a/packages/datasource-sequelize/test/collection.test.ts b/packages/datasource-sequelize/test/collection.test.ts index 836dd5c048..c5ac9442e3 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,33 @@ 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); }); }); describe('list', () => { - const setup = () => { + const setup = (recordData: RecordData[]) => { const { dataSource, name, sequelize } = makeConstructorParams(); const relation = sequelize.define('relation', { @@ -89,11 +95,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 +104,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 +128,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 +294,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 +312,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 +376,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 +471,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 +551,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 +642,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 +718,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 +743,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 +755,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 +783,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 +804,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 +824,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', From 5a0446ec5898ed58645ba8195c58f3e23af9d940 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 6 Jun 2022 17:20:02 +0200 Subject: [PATCH 2/3] fix: some cases --- .../src/utils/serializer.ts | 9 +++++++++ .../test/collection.test.ts | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/datasource-sequelize/src/utils/serializer.ts b/packages/datasource-sequelize/src/utils/serializer.ts index 4d7876fe25..ba869d05fc 100644 --- a/packages/datasource-sequelize/src/utils/serializer.ts +++ b/packages/datasource-sequelize/src/utils/serializer.ts @@ -4,6 +4,8 @@ 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; @@ -12,6 +14,13 @@ export default class Serializer { 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 c5ac9442e3..dcd8ab1deb 100644 --- a/packages/datasource-sequelize/test/collection.test.ts +++ b/packages/datasource-sequelize/test/collection.test.ts @@ -82,6 +82,26 @@ describe('SequelizeDataSource > Collection', () => { ]); 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', () => { From e1d6053c7cc51e04c024c26fdc409f4f56677c09 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 16 Jun 2022 16:39:50 +0200 Subject: [PATCH 3/3] fix: review --- packages/_example/src/forest/customizations/rental.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/_example/src/forest/customizations/rental.ts b/packages/_example/src/forest/customizations/rental.ts index efd77e96c2..8af34c762b 100644 --- a/packages/_example/src/forest/customizations/rental.ts +++ b/packages/_example/src/forest/customizations/rental.ts @@ -8,9 +8,6 @@ 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 = new Date(record.endDate).getTime() - new Date(record.startDate).getTime();