Created
December 10, 2020 12:46
-
-
Save bradleymackey/803538b7b7a416a530b8caee2f027efe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Abstract: | |
* cloning with additional special-hanling logic for Firestore native objects | |
* adapted from klona | |
*/ | |
import * as firebaseAdmin from "firebase-admin"; | |
import * as firebaseTest from "@firebase/rules-unit-testing"; | |
export enum Direction { | |
ToAdmin, | |
ToTest, | |
} | |
const FIRESTORE_VALUE = { | |
fieldValueKeys: new Set(["_delegate"]), | |
// the GeoPoint from "firebase-admin" and "@firebase/testing" have different | |
// internal structure - which is both annoying and confusing! | |
geoPointKeys: new Set(["_lat", "_long"]), | |
realGeoPointKeys: new Set(["_latitude", "_longitude"]), | |
timestampKeys: new Set(["seconds", "nanoseconds"]), | |
}; | |
function sets_equal<T>(as: Set<T>, bs: Set<T>): boolean { | |
if (as.size !== bs.size) return false; | |
for (const a of as) if (!bs.has(a)) return false; | |
return true; | |
} | |
function attemptFirestoreClone(x: any, direction: Direction): any { | |
const allKeys = new Set(Object.keys(x)); | |
// console.log("clone", allKeys); | |
// Firestore FieldValue | |
if (sets_equal(allKeys, FIRESTORE_VALUE.fieldValueKeys)) { | |
// console.log(util.inspect(x)); | |
const methodName = x["_delegate"]["_methodName"]; | |
switch (methodName) { | |
case "FieldValue.serverTimestamp": | |
if (direction === Direction.ToAdmin) { | |
return firebaseAdmin.firestore.FieldValue.serverTimestamp(); | |
} else { | |
return firebaseTest.firestore.FieldValue.serverTimestamp(); | |
} | |
case "FieldValue.increment": | |
const operand = x["_delegate"]["_operand"]; | |
if (direction === Direction.ToAdmin) { | |
return firebaseAdmin.firestore.FieldValue.increment(operand); | |
} else { | |
return firebaseTest.firestore.FieldValue.increment(operand); | |
} | |
case "FieldValue.arrayUnion": | |
const uElements = x["_delegate"]["_elements"]; | |
if (direction === Direction.ToAdmin) { | |
return firebaseAdmin.firestore.FieldValue.arrayUnion(...uElements); | |
} else { | |
return firebaseTest.firestore.FieldValue.arrayUnion(...uElements); | |
} | |
case "FieldValue.arrayRemove": | |
const rElements = x["_delegate"]["_elements"]; | |
if (direction === Direction.ToAdmin) { | |
return firebaseAdmin.firestore.FieldValue.arrayRemove(...rElements); | |
} else { | |
return firebaseTest.firestore.FieldValue.arrayRemove(...rElements); | |
} | |
case "FieldValue.delete": | |
if (direction === Direction.ToAdmin) { | |
return firebaseAdmin.firestore.FieldValue.delete(); | |
} else { | |
return firebaseTest.firestore.FieldValue.delete(); | |
} | |
default: | |
throw Error( | |
`[FIRESTORE FIELDVALUE CLONE ERROR] the _methodName '${methodName}' is unsupported! Just add it now!` | |
); | |
} | |
} | |
// Firestore GeoPoint (from "firebase-admin") | |
if (sets_equal(allKeys, FIRESTORE_VALUE.realGeoPointKeys)) { | |
const latVal: number = x["_latitude"]; | |
const longVal: number = x["_longitude"]; | |
// return the test point, because this is for testing purposes | |
if (direction === Direction.ToAdmin) { | |
return new firebaseAdmin.firestore.GeoPoint(latVal, longVal); | |
} else { | |
return new firebaseTest.firestore.GeoPoint(latVal, longVal); | |
} | |
} | |
// Firestore GeoPoint (from @firebase/rules-unit-testing) | |
if (sets_equal(allKeys, FIRESTORE_VALUE.geoPointKeys)) { | |
// const util = require("util"); | |
// console.log("GEOPOINT VAL", util.inspect(x)); | |
const latVal: number = x["_lat"]; | |
const longVal: number = x["_long"]; | |
if (direction === Direction.ToAdmin) { | |
return new firebaseAdmin.firestore.GeoPoint(latVal, longVal); | |
} else { | |
return new firebaseTest.firestore.GeoPoint(latVal, longVal); | |
} | |
} | |
// Firestore Timestamp | |
if (sets_equal(allKeys, FIRESTORE_VALUE.timestampKeys)) { | |
const seconds: number = x["seconds"]; | |
const nanos: number = x["nanoseconds"]; | |
if (direction === Direction.ToAdmin) { | |
return new firebaseAdmin.firestore.Timestamp(seconds, nanos); | |
} else { | |
return new firebaseTest.firestore.Timestamp(seconds, nanos); | |
} | |
} | |
return undefined; | |
} | |
function set(obj: any, key: any, val: any, direction: Direction): void { | |
if (direction === Direction.ToAdmin) { | |
if (typeof val.value === "object") val.value = cloneToFirebaseAdmin(val.value); | |
} else { | |
if (typeof val.value === "object") val.value = cloneToFirebaseTest(val.value); | |
} | |
if ( | |
!val.enumerable || | |
val.get || | |
val.set || | |
!val.configurable || | |
!val.writable || | |
key === "__proto__" | |
) { | |
Object.defineProperty(obj, key, val); | |
} else obj[key] = val.value; | |
} | |
/** | |
* @param v the object to clone | |
* @param objectOverride a custom closure to invoke for an '[object Object]' that is run before normal clone logic | |
* return undefined from this override function to just continue with normal cloning. | |
*/ | |
export function cloneToFirebaseAdmin<T>(v: T): T { | |
const x: any = v; | |
if (typeof x !== "object") return x; | |
const str = Object.prototype.toString.call(x); | |
let i = 0, | |
k: any, | |
list: any, | |
tmp: any; | |
if (str === "[object Object]") { | |
// CUSTOM FIRESTORE CLONE | |
const firestore = attemptFirestoreClone(x, Direction.ToAdmin); | |
if (firestore) return firestore; | |
// eslint-disable-next-line | |
tmp = Object.create(x.__proto__ || null); | |
} else if (str === "[object Array]") { | |
tmp = Array(x.length); | |
} else if (str === "[object Set]") { | |
tmp = new Set(); | |
x.forEach(function (val: any) { | |
tmp.add(cloneToFirebaseAdmin(val)); | |
}); | |
} else if (str === "[object Map]") { | |
tmp = new Map(); | |
x.forEach(function (val: any, key: any) { | |
tmp.set(cloneToFirebaseAdmin(key), cloneToFirebaseAdmin(val)); | |
}); | |
} else if (str === "[object Date]") { | |
tmp = new Date(+x); | |
} else if (str === "[object RegExp]") { | |
tmp = new RegExp(x.source, x.flags); | |
} else if (str === "[object DataView]") { | |
tmp = new x.constructor(cloneToFirebaseAdmin(x.buffer)); | |
} else if (str === "[object ArrayBuffer]") { | |
tmp = x.slice(0); | |
} else if (str.slice(-6) === "Array]") { | |
// ArrayBuffer.isView(x) | |
// ~> `new` bcuz `Buffer.slice` => ref | |
tmp = new x.constructor(x); | |
} | |
if (tmp) { | |
for (list = Object.getOwnPropertySymbols(x); i < list.length; i++) { | |
set(tmp, list[i], Object.getOwnPropertyDescriptor(x, list[i]), Direction.ToAdmin); | |
} | |
for (i = 0, list = Object.getOwnPropertyNames(x); i < list.length; i++) { | |
if (Object.hasOwnProperty.call(tmp, (k = list[i])) && tmp[k] === x[k]) continue; | |
set(tmp, k, Object.getOwnPropertyDescriptor(x, k), Direction.ToAdmin); | |
} | |
} | |
return tmp || x; | |
} | |
/** | |
* @param v the object to clone | |
* @param objectOverride a custom closure to invoke for an '[object Object]' that is run before normal clone logic | |
* return undefined from this override function to just continue with normal cloning. | |
*/ | |
export function cloneToFirebaseTest<T>(v: T): T { | |
const x: any = v; | |
if (typeof x !== "object") return x; | |
const str = Object.prototype.toString.call(x); | |
let i = 0, | |
k: any, | |
list: any, | |
tmp: any; | |
if (str === "[object Object]") { | |
// CUSTOM FIRESTORE CLONE | |
const firestore = attemptFirestoreClone(x, Direction.ToTest); | |
if (firestore) return firestore; | |
// eslint-disable-next-line | |
tmp = Object.create(x.__proto__ || null); | |
} else if (str === "[object Array]") { | |
tmp = Array(x.length); | |
} else if (str === "[object Set]") { | |
tmp = new Set(); | |
x.forEach(function (val: any) { | |
tmp.add(cloneToFirebaseTest(val)); | |
}); | |
} else if (str === "[object Map]") { | |
tmp = new Map(); | |
x.forEach(function (val: any, key: any) { | |
tmp.set(cloneToFirebaseTest(key), cloneToFirebaseTest(val)); | |
}); | |
} else if (str === "[object Date]") { | |
tmp = new Date(+x); | |
} else if (str === "[object RegExp]") { | |
tmp = new RegExp(x.source, x.flags); | |
} else if (str === "[object DataView]") { | |
tmp = new x.constructor(cloneToFirebaseTest(x.buffer)); | |
} else if (str === "[object ArrayBuffer]") { | |
tmp = x.slice(0); | |
} else if (str.slice(-6) === "Array]") { | |
// ArrayBuffer.isView(x) | |
// ~> `new` bcuz `Buffer.slice` => ref | |
tmp = new x.constructor(x); | |
} | |
if (tmp) { | |
for (list = Object.getOwnPropertySymbols(x); i < list.length; i++) { | |
set(tmp, list[i], Object.getOwnPropertyDescriptor(x, list[i]), Direction.ToTest); | |
} | |
for (i = 0, list = Object.getOwnPropertyNames(x); i < list.length; i++) { | |
if (Object.hasOwnProperty.call(tmp, (k = list[i])) && tmp[k] === x[k]) continue; | |
set(tmp, k, Object.getOwnPropertyDescriptor(x, k), Direction.ToTest); | |
} | |
} | |
return tmp || x; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment