Created
May 16, 2021 11:28
-
-
Save acorn1010/035fd5f529facc7f76996ddaf5449c0a to your computer and use it in GitHub Desktop.
Hacky Firestore onWrite without slow initialization.
This file contains hidden or 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
const service = 'firestore.googleapis.com'; | |
// Note: We avoid importing firebase-functions because we don't want to slow down startup times. | |
type Change<T> = any; | |
type DocumentSnapshot = any; | |
type EventContext = any; | |
type CloudFunction<T> = any; | |
/** | |
* Creates an onWrite function for use as a Firestore onWrite callback. Replaces functions.firestore.document().onWrite(). | |
* @param projectId the Firebase project id for the entire project (e.g. "foo-123"). | |
* @param path a Firestore path such as "usernames/{username}" | |
* @param handler a callback function that'll receive change events. Please note that change.before.ref isn't supported. | |
*/ | |
export function onWrite(projectId: string, path: string, handler: (change: Change<DocumentSnapshot>, context: EventContext) => PromiseLike<any> | any) { | |
const result = async (input: any, context?: any) => { | |
const change: Change<DocumentSnapshot> = { | |
before: createSnapshot(input.oldValue), | |
after: createSnapshot(input.value), | |
}; | |
const handlerContext: EventContext = { | |
...context, | |
resource: {service, name: context.resource}, | |
}; | |
await handler(change, handlerContext); | |
}; | |
result.run = handler; // Why is this needed? | |
result.__trigger = { | |
eventTrigger: { | |
resource: `projects/${projectId}/databases/(default)/documents/${path}`, | |
eventType: 'providers/cloud.firestore/eventTypes/document.write', | |
service, | |
}, | |
}; | |
return result as CloudFunction<Change<DocumentSnapshot>>; | |
} | |
function createSnapshot(value: any): DocumentSnapshot { | |
const data = parseValue(value); | |
new Timestamp(0, 0); | |
return { | |
ref: null as any, | |
exists: !!data, | |
data: () => data, | |
get(fieldPath: string): any { | |
return data && fieldPath in data ? data[fieldPath] : undefined; | |
}, | |
id: '', // TODO(acornwall): What's this? | |
createTime: createTimestamp(value?.createTime ?? null), | |
readTime: createTimestamp(value?.readTime ?? null), | |
updateTime: createTimestamp(value?.updateTime ?? null), | |
isEqual: () => false, // TODO(acornwall): Implement this. | |
}; | |
} | |
function parseValue(value: any): {[key: string]: any} | undefined { | |
const result: {[key: string]: any} = {}; | |
if (!value || !('fields' in value)) { | |
return undefined; | |
} | |
for (const key in value.fields) { | |
result[key] = parseField(value.fields[key]); | |
} | |
return result; | |
} | |
function parseField(field: {[key: string]: any}): {[key: string]: any} | string | null | number | boolean | undefined { | |
const type = Object.keys(field)[0]; | |
if (type === 'mapValue') { | |
return parseValue(field[type]); | |
} else if (type === 'integerValue') { | |
return Number(field[type]); | |
} else if (type === 'booleanValue') { | |
return Boolean(field[type]); | |
} else if (type === 'stringValue') { | |
return field[type]; | |
} else if (type === 'nullValue') { | |
return null; | |
} | |
console.error('UNEXPECTED parseField TYPE: ', type); | |
return field[type]; | |
} | |
function createTimestamp(value: string | null) { | |
if (!value) { | |
return new Timestamp(0, 0); | |
} | |
const milliseconds = new Date(value).getTime(); | |
const subsecondsZ = value.split('.')[1] ?? '0Z'; | |
const subseconds = subsecondsZ.slice(0, subsecondsZ.length - 1); | |
const nanoseconds = +subseconds.padEnd(9, '0'); | |
return new Timestamp(Math.floor(milliseconds / 1000), nanoseconds); | |
} | |
// This is from firebase-admin. We include it here so that we don't need to depend on firebase-admin in order to | |
// minimize cold-start latency. | |
class Timestamp { | |
/** The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. */ | |
readonly seconds: number; | |
/** The non-negative fractions of a second at nanosecond resolution. */ | |
readonly nanoseconds: number; | |
constructor(seconds: number, nanoseconds: number) { | |
this.seconds = seconds; | |
this.nanoseconds = nanoseconds; | |
} | |
/** | |
* Returns a new `Date` corresponding to this timestamp. This may lose | |
* precision. | |
* | |
* @return JavaScript `Date` object representing the same point in time as | |
* this `Timestamp`, with millisecond precision. | |
*/ | |
toDate(): Date { | |
return new Date(this.toMillis()); | |
} | |
/** | |
* Returns the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z. | |
* | |
* @return The point in time corresponding to this timestamp, represented as | |
* the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z. | |
*/ | |
toMillis(): number { | |
return Math.round(this.seconds * 1000 + this.nanoseconds / 1_000_000); | |
} | |
/** | |
* Returns true if this `Timestamp` is equal to the provided one. | |
* | |
* @param other The `Timestamp` to compare against. | |
* @return 'true' if this `Timestamp` is equal to the provided one. | |
*/ | |
isEqual(other: Timestamp): boolean { | |
return this.seconds === other.seconds && this.nanoseconds === other.nanoseconds; | |
} | |
/** | |
* Converts this object to a primitive `string`, which allows `Timestamp` objects to be compared | |
* using the `>`, `<=`, `>=` and `>` operators. | |
* | |
* @return a string encoding of this object. | |
*/ | |
valueOf(): string { | |
return this.toDate().toISOString(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment