From dcfba323a80fdbd5601a489f81f6ec31a57a43c6 Mon Sep 17 00:00:00 2001 From: dk Date: Thu, 28 Sep 2023 17:24:22 +0200 Subject: [PATCH] support to provide value transformer in PropertyTransformHandler (#413) * support to provide value transformer in proxy handler --------- Co-authored-by: Datta Kale --- .../src/__tests__/recordFactory.test.ts | 40 +++++++++++++++++++ packages/salesforce/src/queryRecordFactory.ts | 38 ++++++++++++++---- .../util/src/__tests__/transformProxy.test.ts | 5 +++ packages/util/src/object.ts | 18 +++++++-- 4 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 packages/salesforce/src/__tests__/recordFactory.test.ts diff --git a/packages/salesforce/src/__tests__/recordFactory.test.ts b/packages/salesforce/src/__tests__/recordFactory.test.ts new file mode 100644 index 00000000..3bee6f84 --- /dev/null +++ b/packages/salesforce/src/__tests__/recordFactory.test.ts @@ -0,0 +1,40 @@ +import 'jest'; + +import { RecordFactory } from '../queryRecordFactory'; +import { QueryResult } from '../queryService'; +import { SObjectRecord } from '../types/sobjectRecord'; + +describe('recordFactory', () => { + + describe('#create', () => { + const testRecord: SObjectRecord = { + "attributes" : { + "type" : "Contact", + "url" : "/services/data/v58.0/sobjects/Contact/0037Y00001sJQHdQAO" + }, + "Id" : "0037Y00001sJQHdQAO", + "Name" : "Maria Meyer", + "CreatedDate" : "2023-08-31T13:42:36.000+0000", + "DateOfBirth": "2000-01-01" + }; + + + it('field with full ISO string should be returned as Date instead of string', async () => { + const record = RecordFactory.create>(testRecord); + expect(record.createdDate).toBeInstanceOf(Date); + expect(record.createdDate).toEqual(new Date('2023-08-31T13:42:36.000+0000')); + }); + it('field with ISO string without TZ should be returned as UTC in local TZ', async () => { + const record = RecordFactory.create>(testRecord); + const currentOffset = new Date('2000-01-01').getTimezoneOffset() * 60 * 1000; + const utcExpected = new Date('2000-01-01T00:00:00.000+0000').getTime(); + const localeExpected = new Date(utcExpected + currentOffset); + expect(record.dateOfBirth).toBeInstanceOf(Date); + expect(record.dateOfBirth).toEqual(localeExpected); + }); + it('field with NON ISO string should be returned as is', async () => { + const record = RecordFactory.create>(testRecord); + expect(record.name).toEqual('Maria Meyer'); + }); + }); +}); \ No newline at end of file diff --git a/packages/salesforce/src/queryRecordFactory.ts b/packages/salesforce/src/queryRecordFactory.ts index 4039d2d5..6243ef82 100644 --- a/packages/salesforce/src/queryRecordFactory.ts +++ b/packages/salesforce/src/queryRecordFactory.ts @@ -2,11 +2,12 @@ import { injectable, LifecyclePolicy } from '@vlocode/core'; import { cache, extractNamespaceAndName, normalizeSalesforceName, PropertyTransformHandler, removeNamespacePrefix, substringAfterLast, substringBefore } from '@vlocode/util'; import { QueryResultRecord } from './connection'; import { SalesforceSchemaService } from './salesforceSchemaService'; +import { DateTime } from 'luxon'; export const RecordAttributes = Symbol('attributes'); export const RecordId = Symbol('id'); export const RecordType = Symbol('type'); - +type primitiveDataTypes = string | number | boolean | null | undefined; interface RecordFactoryCreateOptions { /** * Create records using a proxy to intercept property access and transform the property name to the correct casing. @@ -70,7 +71,7 @@ export class RecordFactory { } private createWithProxy(queryResultRecord: QueryResultRecord): T { - return new Proxy(queryResultRecord as any, new PropertyTransformHandler(RecordFactory.getPropertyKey)); + return new Proxy(queryResultRecord as any, new PropertyTransformHandler(RecordFactory.getPropertyKey, RecordFactory.transformValue)); } private createWithDefine(queryResultRecord: QueryResultRecord): T { @@ -97,7 +98,13 @@ export class RecordFactory { } const accessor = { - get: () => queryResultRecord[key], + get: () => { + const value = queryResultRecord[key]; + if(typeof value === 'object') { + return value; + } + return RecordFactory.transformValue(value); + }, set: (value: any) => queryResultRecord[key] = value, enumerable: false, configurable: false @@ -115,16 +122,20 @@ export class RecordFactory { } // Remove relationship properties that are also defined as regular properties + this.removeDuplicateRelationshipProperties(properties, relationships); + + const newProperties = Object.fromEntries( + Object.entries(properties).filter(([key]) => !(key in queryResultRecord))); + return Object.defineProperties(queryResultRecord as T, newProperties); + } + + private removeDuplicateRelationshipProperties(properties: Record, relationships: Array): void { for (const name of relationships) { const commonName = name.slice(0, -3); if (!properties[commonName]) { properties[commonName] = properties[name]; } } - - const newProperties = Object.fromEntries( - Object.entries(properties).filter(([key]) => !(key in queryResultRecord))); - return Object.defineProperties(queryResultRecord as T, newProperties); } @cache({ scope: 'instance', unwrapPromise: true, immutable: false }) @@ -165,6 +176,19 @@ export class RecordFactory { ?? name; } + private static transformValue(value: primitiveDataTypes): unknown { + //string matching iso8601Pattern as Date + if (typeof value === 'string' && + /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(.\d+)?(([+-]\d{2}\d{2})|Z)?)?$/i.test(value)) { + const dateValue = DateTime.fromISO(value); + if (dateValue.isValid) { + return dateValue.toJSDate(); + } + } + + return value; + } + private static generateNormalizedFieldMap(this: void, fields: string[]) { const relationships: Map = new Map(); const fieldMap: Map = new Map(); diff --git a/packages/util/src/__tests__/transformProxy.test.ts b/packages/util/src/__tests__/transformProxy.test.ts index 78828b1a..66bff6e6 100644 --- a/packages/util/src/__tests__/transformProxy.test.ts +++ b/packages/util/src/__tests__/transformProxy.test.ts @@ -53,5 +53,10 @@ describe('util', () => { expect(sliced[0]['b']).toEqual('2'); expect(sut.length).toEqual(3); }); + it('should return transformed value when getting a property value', () => { + const obj = { a: 'foo' }; + const sut = transformPropertyProxy(obj, (target, prop) => prop, (value) => 'bar'); + expect(sut.a).toEqual('bar'); + }); }); }); \ No newline at end of file diff --git a/packages/util/src/object.ts b/packages/util/src/object.ts index bd4f6dad..ca1d8e07 100644 --- a/packages/util/src/object.ts +++ b/packages/util/src/object.ts @@ -4,12 +4,16 @@ import { stringEquals } from './string'; const proxyIdentitySymbol = Symbol('[[proxyIdent]]'); const proxyTargetSymbol = Symbol('[[proxyTarget]]'); +type primitiveDataTypes = string | number | boolean | null | undefined; + export type PropertyTransformer = (target: T, name: string | number | symbol) => string | number | symbol | undefined; +export type ValueTransformer = (value: primitiveDataTypes) => unknown; export class PropertyTransformHandler implements ProxyHandler { constructor( private readonly transformProperty: PropertyTransformer, + private readonly transformValue?: ValueTransformer, private readonly proxyIdentity = randomUUID()) { } @@ -77,8 +81,13 @@ export class PropertyTransformHandler implements ProxyHandler< // or primitive types that cannot be proxied return value; } - return new Proxy(value, new PropertyTransformHandler(this.transformProperty, this.proxyIdentity)); + return new Proxy(value, new PropertyTransformHandler(this.transformProperty, this.transformValue, this.proxyIdentity)); } + + if (this.transformValue) { + return this.transformValue(value); + } + return value; } @@ -110,10 +119,11 @@ export class PropertyTransformHandler implements ProxyHandler< /** * Transforms properties making them accessible according to the transformer function provided through a proxy. * @param target target object - * @param transformer Key/Property transformer + * @param propertyTransformer Key/Property transformer + * @param valueTransformer Value transformer */ -export function transformPropertyProxy(target: T, transformer: PropertyTransformer) : T { - return new Proxy(target, new PropertyTransformHandler(transformer)); +export function transformPropertyProxy(target: T, propertyTransformer: PropertyTransformer, valueTransformer?: ValueTransformer) : T { + return new Proxy(target, new PropertyTransformHandler(propertyTransformer, valueTransformer)); } /**