Skip to content

Instantly share code, notes, and snippets.

@acorn1010
Created May 16, 2021 11:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save acorn1010/035fd5f529facc7f76996ddaf5449c0a to your computer and use it in GitHub Desktop.
Save acorn1010/035fd5f529facc7f76996ddaf5449c0a to your computer and use it in GitHub Desktop.
Hacky Firestore onWrite without slow initialization.
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