Skip to content

Commit

Permalink
support to provide value transformer in PropertyTransformHandler (#413)
Browse files Browse the repository at this point in the history
* support to provide value transformer in proxy handler
---------

Co-authored-by: Datta Kale <[email protected]>
  • Loading branch information
dattakale86 and Datta Kale authored Sep 28, 2023
1 parent f5973e8 commit dcfba32
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 11 deletions.
40 changes: 40 additions & 0 deletions packages/salesforce/src/__tests__/recordFactory.test.ts
Original file line number Diff line number Diff line change
@@ -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<QueryResult<SObjectRecord>>(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<QueryResult<SObjectRecord>>(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<QueryResult<SObjectRecord>>(testRecord);
expect(record.name).toEqual('Maria Meyer');
});
});
});
38 changes: 31 additions & 7 deletions packages/salesforce/src/queryRecordFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -70,7 +71,7 @@ export class RecordFactory {
}

private createWithProxy<T extends object>(queryResultRecord: QueryResultRecord): T {
return new Proxy<T>(queryResultRecord as any, new PropertyTransformHandler(RecordFactory.getPropertyKey));
return new Proxy<T>(queryResultRecord as any, new PropertyTransformHandler(RecordFactory.getPropertyKey, RecordFactory.transformValue));
}

private createWithDefine<T extends object>(queryResultRecord: QueryResultRecord): T {
Expand All @@ -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
Expand All @@ -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<string, PropertyDescriptor>, relationships: Array<string>): 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 })
Expand Down Expand Up @@ -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<string, string> = new Map<string, string>();
const fieldMap: Map<string, string> = new Map<string, string>();
Expand Down
5 changes: 5 additions & 0 deletions packages/util/src/__tests__/transformProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
18 changes: 14 additions & 4 deletions packages/util/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = (target: T, name: string | number | symbol) => string | number | symbol | undefined;
export type ValueTransformer = (value: primitiveDataTypes) => unknown;

export class PropertyTransformHandler<T extends object> implements ProxyHandler<T> {

constructor(
private readonly transformProperty: PropertyTransformer<T>,
private readonly transformValue?: ValueTransformer,
private readonly proxyIdentity = randomUUID()) {
}

Expand Down Expand Up @@ -77,8 +81,13 @@ export class PropertyTransformHandler<T extends object> 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;
}

Expand Down Expand Up @@ -110,10 +119,11 @@ export class PropertyTransformHandler<T extends object> 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<T extends object>(target: T, transformer: PropertyTransformer<T>) : T {
return new Proxy(target, new PropertyTransformHandler(transformer));
export function transformPropertyProxy<T extends object>(target: T, propertyTransformer: PropertyTransformer<T>, valueTransformer?: ValueTransformer) : T {
return new Proxy(target, new PropertyTransformHandler(propertyTransformer, valueTransformer));
}

/**
Expand Down

0 comments on commit dcfba32

Please sign in to comment.