diff --git a/src/backend/tests/utils/strings.test.ts b/src/backend/tests/utils/strings.test.ts index caec15c1..2a3f85ec 100644 --- a/src/backend/tests/utils/strings.test.ts +++ b/src/backend/tests/utils/strings.test.ts @@ -133,6 +133,18 @@ it('Proxy object has case-insensitive keys', function() { expect(iObj.foo).eq('bar'); }); +it('Proxy object handles spread', function() { + const myObj = {FOO: 'bar', WIDget: 'cool'}; + + const iObj = noCasePropObj(myObj); + const { foo, ...rest } = iObj; + + expect(foo).is.not.undefined; + expect(foo).eq('bar'); + expect(rest.widget).is.not.undefined; + expect(rest.widget).eq('cool'); +}); + describe('Interpolation', function() { it('interpolates values', function() { diff --git a/src/backend/utils/DataUtils.ts b/src/backend/utils/DataUtils.ts index 60480bac..5f1567d3 100644 --- a/src/backend/utils/DataUtils.ts +++ b/src/backend/utils/DataUtils.ts @@ -15,6 +15,16 @@ export const asArray = (data: T | T[]): T[] => { const handler: ProxyHandler = { + ownKeys(target) { + return Reflect.ownKeys(target).map(x => typeof x === 'string' ? x.toLocaleLowerCase() : x); + }, + getOwnPropertyDescriptor(target, key) { + return { + ...Reflect.getOwnPropertyDescriptor(target, key), + enumerable: true, + configurable: true, + }; + }, get: function (target, key) { //console.log("key: " + key.toString()); if (typeof key == "string") { @@ -65,30 +75,36 @@ const checkAtomic = (value) => { return value; } +export type toLowerCase = { + [K in keyof T]: T[K] +} + +export type LowercaseString = S extends `${infer Str}` + ? `${Lowercase}` + : S; + +/** https://medium.com/@vincent.dibon_78881/type-level-uppercase-and-lowercase-in-typescript-c18f574f4572 */ +export type LowercaseKeys = { + [K in keyof T as LowercaseString>]: T[K] +}; + /** Return a Proxy of an object where keys can be accessed case-insensitive * - * https://stackoverflow.com/a/50102779/1469797 + * Note: objects from rest operator in spread are *always* lowercase + * + * @see https://stackoverflow.com/a/50102779/1469797 */ -export const noCasePropObj = (obj: T): T => { - let newObj; - - if (typeof obj == "object") { - newObj = new Proxy({}, handler); - // traverse the Original object converting string keys to upper case - for (var key in obj) { - if (typeof key == "string") { - var objKey = key.toUpperCase(); - if (!(key in newObj)) - newObj[objKey] = checkAtomic(obj[key]); - } +export const noCasePropObj = (obj: T): LowercaseKeys => { + + const newObj = new Proxy>({} as LowercaseKeys, handler); + // traverse the Original object converting string keys to upper case + for (var key in obj) { + if (typeof key == "string") { + var objKey = key.toUpperCase(); + if (!(key in newObj)) + newObj[objKey] = checkAtomic(obj[key]); } } - else if (Array.isArray(obj)) { - // in an array of objects convert to upper case string keys within each row - newObj = new Array(); - for (var i = 0; i < obj.length; i++) - newObj[i] = checkAtomic(obj[i]); - } return newObj; // object with upper cased keys } export async function readJson(this: any, path: any, options: ReadJsonOptions = {}) {