Skip to content

Commit

Permalink
add batchwrite functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
abkarino committed Oct 5, 2022
1 parent ad24ec3 commit bec85d4
Show file tree
Hide file tree
Showing 14 changed files with 4,570 additions and 1,108 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.clasp.json
.vs
.idea
gapps.config.json
node_modules
17 changes: 17 additions & 0 deletions Firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete {
return this.query_(path, request);
}
query_ = FirestoreRead.prototype.query_;

/**
* Create a batch update
*/
batch(): WriteBatch {
return new WriteBatch(this);
}

/**
* Generates a new unique id
* Useful for WriteBatch create document
*
* @return {string} unique doc id
*/
newId(): string {
return Util_.newId();
}
}

type Version = 'v1' | 'v1beta1' | 'v1beta2';
Expand Down
32 changes: 32 additions & 0 deletions Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,36 @@ class Util_ {
.map(([k, v]) => `${process(k)}=${process(v)}`)
.join('&');
}

/**
* Create a new unique document id
*
* @return {string} a new unique document id
* @link https://github.com/firebase/firebase-js-sdk/blob/34ad43cc2a9863f7ac326c314d9539fcbc1f0913/packages/firestore/src/util/misc.ts#L28
*/
static newId(): string {
// Alphanumeric characters
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// The largest byte value that is a multiple of `char.length`.
const maxMultiple = Math.floor(256 / chars.length) * chars.length;

let autoId = '';
const targetLength = 20;
while (autoId.length < targetLength) {
const nBytes = 40;
const bytes = new Uint8Array(nBytes);
for (let i = 0; i < nBytes; i++) {
bytes[i] = Math.floor(Math.random() * 256);
}
for (let i = 0; i < bytes.length; ++i) {
// Only accept values that are [0, maxMultiple), this ensures they can
// be evenly mapped to indices of `chars` via a modulo operation.
if (autoId.length < targetLength && bytes[i] < maxMultiple) {
autoId += chars.charAt(bytes[i] % chars.length);
}
}
}

return autoId;
}
}
162 changes: 162 additions & 0 deletions WriteBatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Write multiple documents in one request.
* Can be atomic or not.
*/
class WriteBatch {
private mutations_: FirestoreAPI.Write[] = [];
private committed_ = false;

/**
* A container for multiple writes
* @param {Firestore} _firestore the parent instance
* @param {Boolean} _atomic the REST api supports batch writes in a non-atomic way
*/
constructor(private readonly _firestore: Firestore) {}

/**
* Writes to the document referred to by the provided path.
* If the document does not exist yet, it will be created.
*
* @param {string} path - A path to the document to be set.
* @param data - An object of the fields and values for the document.
* @returns This `WriteBatch` instance. Used for chaining method calls.
*/
set(path: string, fields: Record<string, any>): WriteBatch;
/**
* Writes to the document referred to by the provided path.
* If the document does not exist yet, it will be created.
* If you provide `merge` or `mergeFields`, the provided data can be merged
* into an existing document.
*
* @param {string} path - A path to the document to be set.
* @param data - An object of the fields and values for the document.
* @param options - An object to configure the set behavior.
* @throws Error - If the provided input is not a valid Firestore document.
* @returns This `WriteBatch` instance. Used for chaining method calls.
*/
set(path: string, fields: Record<string, any>, options: Record<string, any>): WriteBatch;
set(
path: string,
fields: Record<string, any>,
options?: Record<string, any> // FirestoreAPI.SetOptions
): WriteBatch {
this.verifyNotCommitted_();

const isMerge = options && (options.merge || options.mergeFields);
const updateMask: FirestoreAPI.DocumentMask | undefined = isMerge
? { fieldPaths: options.merge ? Object.keys(fields) : options.mergeFields }
: undefined;
const update: FirestoreAPI.Document = new Document(fields, `${this._firestore.basePath}${path}`);

const mutation: FirestoreAPI.Write = {
updateMask: updateMask,
update: update,
};
this.mutations_.push(mutation);
return this;
}

/**
* Updates fields in the document referred to by the provided {@link
* DocumentReference}. The update will fail if applied to a document that does
* not exist.
*
* @param {string} path - A path to the document to be updated.
* @param data - An object containing the fields and values with which to
* update the document. Fields can contain dots to reference nested fields
* within the document.
* @throws Error - If the provided input is not valid Firestore data.
* @returns This `WriteBatch` instance. Used for chaining method calls.
*/
update(path: string, data: Record<string, any>): WriteBatch;
/**
* Updates fields in the document referred to by the provided path.
* The update will fail if applied to a document that does
* not exist.
*
* Nested fields can be update by providing dot-separated field path strings.
*
* @param {string} path - A path to the document to be updated.
* @param field - The first field to update.
* @param value - The first value.
* @param moreFieldsAndValues - Additional key value pairs.
* @throws Error - If the provided input is not valid Firestore data.
* @returns This `WriteBatch` instance. Used for chaining method calls.
*/
update(path: string, field: string, value: unknown, ...moreFieldsAndValues: unknown[]): WriteBatch;
update(
path: string,
fieldOrUpdateData: string | Record<string, any>,
value?: unknown,
...moreFieldsAndValues: unknown[]
): WriteBatch {
this.verifyNotCommitted_();

let fields;
if (typeof fieldOrUpdateData === 'string') {
fields = {
[fieldOrUpdateData]: value,
};
for (let i = 0; i < moreFieldsAndValues.length; i += 2) {
fields[moreFieldsAndValues[i] as string] = moreFieldsAndValues[i + 1];
}
} else fields = fieldOrUpdateData;
const updateMask: FirestoreAPI.DocumentMask = { fieldPaths: Object.keys(fields) };
const update: FirestoreAPI.Document = new Document(fields, `${this._firestore.basePath}${path}`);

const mutation: FirestoreAPI.Write = {
updateMask: updateMask,
update: update,
currentDocument: {
exists: true,
},
};
this.mutations_.push(mutation);
return this;
}

/**
* Deletes the document referred to by the provided path.
*
* @param {string} path - A path to the document to be deleted.
* @returns This `WriteBatch` instance. Used for chaining method calls.
*/
delete(path: string): WriteBatch {
this.verifyNotCommitted_();
// const ref = validateReference(documentRef, this._firestore);
// new DeleteMutation(ref._key, Precondition.none())
const mutation: FirestoreAPI.Write = {
delete: `${this._firestore.basePath}${path}`,
};
this.mutations_.push(mutation);
return this;
}

/**
* Issue the write request.
* If atomic, a true value is returned on success or throws an error on failure.
* If not atomic, an array of either true or error message is returned.
*/
commit(atomic = false): boolean | Array<true | string | undefined> {
this.verifyNotCommitted_();
this.committed_ = true;
if (!this.mutations_.length) {
throw new Error('A write batch cannot commit with no changes requested.');
}

const request = new Request(this._firestore.baseUrl, this._firestore.authToken);
request.route(atomic ? 'commit' : 'batchWrite');

const payload: FirestoreAPI.CommitRequest | FirestoreAPI.BatchWriteRequest = { writes: this.mutations_ };
const responseObj = request.post<FirestoreAPI.CommitResponse | FirestoreAPI.BatchWriteResponse>(undefined, payload);

if (atomic) return true;
else return ((responseObj as FirestoreAPI.BatchWriteResponse).status || []).map((s) => !s.code || s.message);
}

private verifyNotCommitted_(): void {
if (this.committed_) {
throw new Error('A write batch can no longer be used after commit() ' + 'has been called.');
}
}
}
Loading

0 comments on commit bec85d4

Please sign in to comment.