Skip to content

Instantly share code, notes, and snippets.

@bradleymackey
Created December 10, 2020 12:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bradleymackey/803538b7b7a416a530b8caee2f027efe to your computer and use it in GitHub Desktop.
Save bradleymackey/803538b7b7a416a530b8caee2f027efe to your computer and use it in GitHub Desktop.
/**
* 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