UNPKG

@firestore-emulator/server

Version:

This package is the implementation of the Firestore emulator. It is a Node.js

1,511 lines (1,503 loc) 53.3 kB
// src/FirestoreState/index.ts import { EventEmitter } from "node:events"; import { Document as v1Document } from "@firestore-emulator/proto/dist/google/firestore/v1/document"; import { ListenResponse as v1ListenResponse, TargetChangeTargetChangeType as v1TargetChangeTargetChangeType } from "@firestore-emulator/proto/dist/google/firestore/v1/firestore"; import { StructuredQueryCompositeFilterOperator as v1StructuredQueryCompositeFilterOperator, StructuredQueryDirection as v1StructuredQueryDirection, StructuredQueryFieldFilterOperator as v1StructuredQueryFieldFilterOperator } from "@firestore-emulator/proto/dist/google/firestore/v1/query"; import { DocumentTransformFieldTransformServerValue, WriteResult as v1WriteResult } from "@firestore-emulator/proto/dist/google/firestore/v1/write"; import { Timestamp } from "@firestore-emulator/proto/dist/google/protobuf/timestamp"; import { Status } from "@grpc/grpc-js/build/src/constants"; import { assertNever } from "assert-never"; import { produce as produce2 } from "immer"; // src/error/error.ts var FirestoreEmulatorError = class extends Error { constructor(code, message) { super(message); this.code = code; } }; // src/utils.ts var isNotNull = (value) => value != null; // src/FirestoreState/field.ts import { NullValue } from "@firestore-emulator/proto/dist/google/protobuf/struct"; var FirestoreStateDocumentStringField = class _FirestoreStateDocumentStringField { constructor(value) { this.value = value; } type = "string_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { string_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentStringField && this.value === other.value; } lt(other) { return other instanceof _FirestoreStateDocumentStringField && this.value < other.value; } lte(other) { return other instanceof _FirestoreStateDocumentStringField && this.value <= other.value; } gt(other) { return other instanceof _FirestoreStateDocumentStringField && this.value > other.value; } gte(other) { return other instanceof _FirestoreStateDocumentStringField && this.value >= other.value; } }; var FirestoreStateDocumentNullField = class { type = "null_value"; value = null; toJSON() { return { type: this.type, value: null }; } toV1ValueObject() { return { null_value: NullValue.NULL_VALUE }; } eq(other) { return other.type === this.type; } lt(_other) { return false; } lte(_other) { return false; } gt(_other) { return false; } gte(_other) { return false; } }; var FirestoreStateDocumentBooleanField = class _FirestoreStateDocumentBooleanField { constructor(value) { this.value = value; } type = "boolean_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { boolean_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentBooleanField && this.value === other.value; } lt(_other) { return false; } lte(_other) { return false; } gt(_other) { return false; } gte(_other) { return false; } }; var FirestoreStateDocumentIntegerField = class _FirestoreStateDocumentIntegerField { constructor(value) { this.value = value; if (!Number.isInteger(value)) { throw new Error(`value must be integer. value=${value}`); } } type = "integer_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { integer_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentIntegerField && this.value === other.value; } lt(other) { return other instanceof _FirestoreStateDocumentIntegerField && this.value < other.value; } lte(other) { return other instanceof _FirestoreStateDocumentIntegerField && this.value <= other.value; } gt(other) { return other instanceof _FirestoreStateDocumentIntegerField && this.value > other.value; } gte(other) { return other instanceof _FirestoreStateDocumentIntegerField && this.value >= other.value; } add(other) { if (other instanceof _FirestoreStateDocumentIntegerField) { return new _FirestoreStateDocumentIntegerField(this.value + other.value); } if (other instanceof FirestoreStateDocumentDoubleField) { return new FirestoreStateDocumentDoubleField(this.value + other.value); } throw new Error(`unsupported type. other=${other}`); } }; var FirestoreStateDocumentDoubleField = class _FirestoreStateDocumentDoubleField { constructor(value) { this.value = value; } type = "double_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { double_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentDoubleField && this.value === other.value; } lt(other) { return other instanceof _FirestoreStateDocumentDoubleField && this.value < other.value; } lte(other) { return other instanceof _FirestoreStateDocumentDoubleField && this.value <= other.value; } gt(other) { return other instanceof _FirestoreStateDocumentDoubleField && this.value > other.value; } gte(other) { return other instanceof _FirestoreStateDocumentDoubleField && this.value >= other.value; } add(other) { if (other instanceof FirestoreStateDocumentIntegerField) { return new _FirestoreStateDocumentDoubleField(this.value + other.value); } if (other instanceof _FirestoreStateDocumentDoubleField) { return new _FirestoreStateDocumentDoubleField(this.value + other.value); } throw new Error(`unsupported type. other=${other}`); } }; var FirestoreStateDocumentTimestampField = class _FirestoreStateDocumentTimestampField { constructor(value) { this.value = value; } type = "timestamp_value"; static fromDate(date) { return new _FirestoreStateDocumentTimestampField({ nanos: date.getTime() % 1e3 * 1e6, seconds: Math.floor(date.getTime() / 1e3) }); } toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { timestamp_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentTimestampField && this.value.seconds === other.value.seconds && this.value.nanos === other.value.nanos; } lt(other) { return other instanceof _FirestoreStateDocumentTimestampField && (this.value.seconds < other.value.seconds || this.value.seconds === other.value.seconds && this.value.nanos < other.value.nanos); } lte(other) { return other instanceof _FirestoreStateDocumentTimestampField && (this.value.seconds < other.value.seconds || this.value.seconds === other.value.seconds && this.value.nanos <= other.value.nanos); } gt(other) { return other instanceof _FirestoreStateDocumentTimestampField && (this.value.seconds > other.value.seconds || this.value.seconds === other.value.seconds && this.value.nanos > other.value.nanos); } gte(other) { return other instanceof _FirestoreStateDocumentTimestampField && (this.value.seconds > other.value.seconds || this.value.seconds === other.value.seconds && this.value.nanos >= other.value.nanos); } }; var FirestoreStateDocumentBytesField = class _FirestoreStateDocumentBytesField { constructor(value) { this.value = value; } type = "bytes_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { bytes_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentBytesField && this.value.toString() === other.value.toString(); } lt(other) { return other instanceof _FirestoreStateDocumentBytesField && this.value.toString() < other.value.toString(); } lte(other) { return other instanceof _FirestoreStateDocumentBytesField && this.value.toString() <= other.value.toString(); } gt(other) { return other instanceof _FirestoreStateDocumentBytesField && this.value.toString() > other.value.toString(); } gte(other) { return other instanceof _FirestoreStateDocumentBytesField && this.value.toString() >= other.value.toString(); } }; var FirestoreStateDocumentReferenceField = class _FirestoreStateDocumentReferenceField { constructor(value) { this.value = value; } type = "reference_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { reference_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentReferenceField && this.value === other.value; } lt(_other) { return false; } lte(_other) { return false; } gt(_other) { return false; } gte(_other) { return false; } }; var FirestoreStateDocumentGeoPointField = class _FirestoreStateDocumentGeoPointField { constructor(value) { this.value = value; } type = "geo_point_value"; toJSON() { return { type: this.type, value: this.value }; } toV1ValueObject() { return { geo_point_value: this.value }; } eq(other) { return other instanceof _FirestoreStateDocumentGeoPointField && this.value.latitude === other.value.latitude && this.value.longitude === other.value.longitude; } lt(other) { return other instanceof _FirestoreStateDocumentGeoPointField && (this.value.latitude < other.value.latitude || this.value.longitude < other.value.longitude); } lte(other) { return other instanceof _FirestoreStateDocumentGeoPointField && (this.value.latitude <= other.value.latitude || this.value.longitude <= other.value.longitude); } gt(other) { return other instanceof _FirestoreStateDocumentGeoPointField && (this.value.latitude > other.value.latitude || this.value.longitude > other.value.longitude); } gte(other) { return other instanceof _FirestoreStateDocumentGeoPointField && (this.value.latitude >= other.value.latitude || this.value.longitude >= other.value.longitude); } }; var FirestoreStateDocumentArrayField = class _FirestoreStateDocumentArrayField { constructor(value) { this.value = value; } type = "array_value"; toJSON() { return { type: this.type, value: this.value.map((v) => v.toJSON()) }; } toV1ValueObject() { return { array_value: { values: this.value.map((v) => v.toV1ValueObject()) } }; } eq(other) { return other instanceof _FirestoreStateDocumentArrayField && this.value.length === other.value.length && this.value.every((v, i) => { const item = other.value[i]; if (!item) return false; return item.eq(v); }); } lt(_other) { return false; } lte(_other) { return false; } gt(_other) { return false; } gte(_other) { return false; } }; var FirestoreStateDocumentMapField = class _FirestoreStateDocumentMapField { constructor(value) { this.value = value; } type = "map_value"; toJSON() { return { type: this.type, value: Object.fromEntries( Object.entries(this.value).map(([k, v]) => [k, v.toJSON()]) ) }; } toV1ValueObject() { return { map_value: { fields: Object.fromEntries( Object.entries(this.value).map(([k, v]) => [k, v.toV1ValueObject()]) ) } }; } eq(other) { return other instanceof _FirestoreStateDocumentMapField && Object.keys(this.value).length === Object.keys(other.value).length && Object.entries(this.value).every(([k, v]) => { const item = other.value[k]; if (!item) return false; return item.eq(v); }); } lt(_other) { return false; } lte(_other) { return false; } gt(_other) { return false; } gte(_other) { return false; } }; var convertV1DocumentField = (field) => { if (field.has_string_value) return new FirestoreStateDocumentStringField(field.string_value); if (field.has_null_value) return new FirestoreStateDocumentNullField(); if (field.has_boolean_value) return new FirestoreStateDocumentBooleanField(field.boolean_value); if (field.has_integer_value) return new FirestoreStateDocumentIntegerField(field.integer_value); if (field.has_double_value) return new FirestoreStateDocumentDoubleField(field.double_value); if (field.has_timestamp_value) return new FirestoreStateDocumentTimestampField({ nanos: field.timestamp_value.nanos, seconds: field.timestamp_value.seconds }); if (field.has_bytes_value) return new FirestoreStateDocumentBytesField(field.bytes_value); if (field.has_reference_value) return new FirestoreStateDocumentReferenceField(field.reference_value); if (field.has_geo_point_value) return new FirestoreStateDocumentGeoPointField({ latitude: field.geo_point_value.latitude, longitude: field.geo_point_value.longitude }); if (field.has_array_value) return new FirestoreStateDocumentArrayField( field.array_value.values.map(convertV1DocumentField) ); if (field.has_map_value) return new FirestoreStateDocumentMapField( Object.fromEntries( Array.from(field.map_value.fields.entries()).map(([k, v]) => [ k, convertV1DocumentField(v) ]) ) ); throw new Error(`unknown field type. field=${JSON.stringify(field)}`); }; var convertV1Value = (value) => { return convertV1DocumentField(value).toV1ValueObject(); }; // src/FirestoreState/mask.ts import { produce } from "immer"; function escapeKey(key) { return key.includes(".") ? `\`${key}\`` : key; } function getNestedUpdateMask(updateMask, key) { const escapedKey = escapeKey(key); return updateMask.filter((m) => m.startsWith(`${escapedKey}.`)).map((m) => m.substring(escapedKey.length + 1)); } function isUpdatable(updateMask, key) { return updateMask.length === 0 || updateMask.includes(escapeKey(key)); } function updateFields(current, fields, updateMask) { return produce(current, (draft) => { for (const [key, field] of Object.entries(fields)) { const updatable = isUpdatable(updateMask, key); const nestedUpdateMask = getNestedUpdateMask(updateMask, key); if (updatable) { draft[key] = field; } else if (field.type === "map_value" && (draft[key] == null || draft[key].type === "map_value")) { draft[key] = new FirestoreStateDocumentMapField( updateFields(draft[key]?.value ?? {}, field.value, nestedUpdateMask) ); } } }); } // src/FirestoreState/index.ts var FirestoreStateDocument = class _FirestoreStateDocument { constructor(emitter, database, parent, name, fields, collections) { this.emitter = emitter; this.database = database; this.parent = parent; this.name = name; this.fields = fields; this.collections = collections; } metadata = { createdAt: null, hasExist: false, updatedAt: null }; iterateFromRoot() { const iters = [this]; let current = this; while (current.parent.parent instanceof _FirestoreStateDocument) { const next = current.parent.parent; current = next; iters.push(current); } return iters.slice().reverse(); } hasChild() { for (const collection of Object.values(this.collections)) { if (collection.hasChild()) return true; } return false; } getCollection(collectionName) { this.collections = produce2(this.collections, (draft) => { if (!(collectionName in draft)) { const collection2 = new FirestoreStateCollection( this.emitter, this.database, this, collectionName, {} ); draft[collectionName] = collection2; this.emitter.emit("add-collection", { collection: collection2 }); } }); const collection = this.collections[collectionName]; if (!collection) { throw new Error(`Collection<${collectionName}> not found.`); } return collection; } toV1Document() { return v1Document.fromObject({ create_time: this.metadata.hasExist ? Timestamp.fromObject({ nanos: this.metadata.createdAt.getMilliseconds() * 1e3 * 1e3, seconds: Math.floor(this.metadata.createdAt.getTime() / 1e3) }) : void 0, fields: Object.fromEntries( Object.entries(this.fields).map(([key, field]) => [ key, field.toV1ValueObject() ]) ), name: this.getPath(), update_time: this.metadata.hasExist ? Timestamp.fromObject({ nanos: this.metadata.updatedAt.getMilliseconds() * 1e3 * 1e3, seconds: Math.floor(this.metadata.updatedAt.getTime() / 1e3) }) : void 0 }); } toV1DocumentObject() { const document = this.toV1Document(); return { create_time: document.create_time.toObject(), fields: Object.fromEntries( Array.from(document.fields).map(([key, value]) => [ key, convertV1Value(value) ]) ), name: document.name, update_time: document.update_time.toObject() }; } toJSON() { return { collections: Object.fromEntries( Object.entries(this.collections).filter(([, collection]) => collection.hasChild()).map(([key, collection]) => [key, collection.toJSON()]) ), fields: Object.fromEntries( Object.entries(this.fields).map(([key, field]) => [ key, field.toJSON() ]) ), path: this.getPath() }; } create(date, fields) { if (this.metadata.hasExist) { throw new Error("Document already exists."); } this.metadata = produce2(this.metadata, (draft) => { draft.hasExist = true; draft.createdAt = date; draft.updatedAt = date; }); this.fields = produce2(this.fields, (draft) => { for (const [key, field] of Object.entries(fields)) { draft[key] = field; } }); this.emitter.emit("create-document", { document: this }); } update(date, fields, updateMask = []) { if (!this.metadata.hasExist) { throw new Error("Document does not exist."); } this.metadata = produce2(this.metadata, (draft) => { draft.updatedAt = date; }); this.fields = updateFields(this.fields, fields, updateMask); this.emitter.emit("update-document", { document: this }); } set(date, fields, updateMask = []) { const isCreate = !this.metadata.hasExist; if (!this.metadata.hasExist) { this.metadata = produce2(this.metadata, (draft) => { draft.hasExist = true; draft.createdAt = date; draft.updatedAt = date; }); } else { this.metadata = produce2(this.metadata, (draft) => { draft.updatedAt = date; }); } this.fields = updateFields(this.fields, fields, updateMask); if (isCreate) { this.emitter.emit("create-document", { document: this }); } else { this.emitter.emit("update-document", { document: this }); } } delete() { if (!this.metadata.hasExist) { throw new Error("Document does not exist."); } this.metadata = produce2(this.metadata, (draft) => { draft.hasExist = false; draft.createdAt = null; draft.updatedAt = null; }); this.fields = produce2(this.fields, (draft) => { for (const key of Object.keys(draft)) { delete draft[key]; } }); this.emitter.emit("delete-document", { document: this }); } getField(path) { if (path.includes(".")) { throw new Error("Not implemented"); } return this.fields[path]; } getPath() { return `${this.parent.getPath()}/${this.name}`; } getDocumentPath() { return `${this.parent.getDocumentPath()}/${this.name}`; } v1Transform(date, transforms) { for (const transform of transforms) { const field = this.getField(transform.field_path); if (transform.has_increment) { if (transform.increment.has_integer_value) { if (!field) { this.set(date, { [transform.field_path]: new FirestoreStateDocumentIntegerField( transform.increment.integer_value ) }); continue; } if (field instanceof FirestoreStateDocumentIntegerField) { this.set(date, { [transform.field_path]: new FirestoreStateDocumentIntegerField( field.value + transform.increment.integer_value ) }); continue; } if (field instanceof FirestoreStateDocumentDoubleField) { this.set(date, { [transform.field_path]: new FirestoreStateDocumentDoubleField( field.value + transform.increment.integer_value ) }); continue; } throw new Error( `Invalid transform: ${JSON.stringify( transform )}. increment transform can only be applied to an integer or a double field` ); } if (transform.increment.has_double_value) { if (!field) { this.set(date, { [transform.field_path]: new FirestoreStateDocumentDoubleField( transform.increment.double_value ) }); continue; } if (field instanceof FirestoreStateDocumentIntegerField || field instanceof FirestoreStateDocumentDoubleField) { this.set(date, { [transform.field_path]: new FirestoreStateDocumentDoubleField( field.value + transform.increment.double_value ) }); continue; } throw new Error( `Invalid transform: ${JSON.stringify( transform )}. increment transform can only be applied to an integer or a double field` ); } throw new Error( `Invalid transform: ${JSON.stringify( transform )}. increment transform can only be applied to an integer or a double field` ); } if (transform.has_remove_all_from_array) { const removeFields = transform.remove_all_from_array.values.map( convertV1DocumentField ); if (field instanceof FirestoreStateDocumentArrayField) { const removedFields = field.value.filter( (value) => !removeFields.some((removeField) => removeField.eq(value)) ); this.set(date, { [transform.field_path]: new FirestoreStateDocumentArrayField( removedFields ) }); return; } } if (transform.has_append_missing_elements) { const appendFields = transform.append_missing_elements.values.map( convertV1DocumentField ); if (field instanceof FirestoreStateDocumentArrayField) { const appendedFields = [ ...field.value, ...appendFields.filter( (appendField) => !field.value.some((value) => value.eq(appendField)) ) ]; this.set(date, { [transform.field_path]: new FirestoreStateDocumentArrayField( appendedFields ) }); return; } } if (transform.has_set_to_server_value) { if (transform.set_to_server_value === DocumentTransformFieldTransformServerValue.REQUEST_TIME) { this.set(date, { [transform.field_path]: FirestoreStateDocumentTimestampField.fromDate(date) }); return; } throw new Error( `Invalid transform: ${JSON.stringify( transform )}. set_to_server_value must be a valid value` ); } throw new Error( `Invalid transform: ${JSON.stringify(transform.toObject(), null, 4)}` ); } } }; var FirestoreStateCollection = class { constructor(emitter, database, parent, name, documents) { this.emitter = emitter; this.database = database; this.parent = parent; this.name = name; this.documents = documents; } getDocument(documentName) { this.documents = produce2(this.documents, (draft) => { if (!(documentName in draft)) { const document2 = new FirestoreStateDocument( this.emitter, this.database, this, documentName, {}, {} ); draft[documentName] = document2; } }); const document = this.documents[documentName]; if (!document) { throw new Error(`Document<${documentName}> not found.`); } return document; } hasChild() { for (const document of Object.values(this.documents)) { if (document.metadata.hasExist) return true; if (document.hasChild()) return true; } return false; } getAllDocuments() { return Object.values(this.documents); } toJSON() { return { documents: Object.fromEntries( Object.entries(this.documents).filter( ([, document]) => document.metadata.hasExist || document.hasChild() ).map(([key, document]) => [key, document.toJSON()]) ), path: this.getPath() }; } getPath() { return `${this.parent.getPath()}/${this.name}`; } getDocumentPath() { if (this.parent instanceof FirestoreStateDatabase) return this.name; return `${this.parent.getDocumentPath()}/${this.name}`; } }; var FirestoreStateDatabase = class { constructor(emitter, project, name, collections) { this.emitter = emitter; this.project = project; this.name = name; this.collections = collections; } getCollection(collectionName) { this.collections = produce2(this.collections, (draft) => { if (!(collectionName in draft)) { const collection2 = new FirestoreStateCollection( this.emitter, this, this, collectionName, {} ); draft[collectionName] = collection2; this.emitter.emit("add-collection", { collection: collection2 }); } }); const collection = this.collections[collectionName]; if (!collection) { throw new Error(`Collection<${collectionName}> not found.`); } return collection; } toJSON() { return { collections: Object.fromEntries( Object.entries(this.collections).map(([key, collection]) => [ key, collection.toJSON() ]) ), path: this.getPath() }; } getPath() { return `${this.project.getPath()}/databases/${this.name}/documents`; } }; var FirestoreStateProject = class { constructor(emitter, name, databases) { this.emitter = emitter; this.name = name; this.databases = databases; } toJSON() { return { databases: Object.fromEntries( Object.entries(this.databases).map(([key, database]) => [ key, database.toJSON() ]) ), path: this.getPath() }; } getPath() { return `projects/${this.name}`; } getDatabase(databaseName) { this.databases = produce2(this.databases, (draft) => { if (!(databaseName in draft)) { const database2 = new FirestoreStateDatabase( this.emitter, this, databaseName, {} ); draft[databaseName] = database2; this.emitter.emit("add-database", { database: database2 }); } }); const database = this.databases[databaseName]; if (!database) { throw new Error(`Database<${databaseName}> not found.`); } return database; } }; var TimestampFromDate = (date) => Timestamp.fromObject({ nanos: date.getTime() % 1e3 * 1e3 * 1e3, seconds: Math.floor(date.getTime() / 1e3) }); var DateFromTimestamp = (timestamp) => new Date(timestamp.seconds * 1e3 + timestamp.nanos / 1e3 / 1e3); var TimestampFromNow = () => TimestampFromDate(/* @__PURE__ */ new Date()); var v1FilterDocuments = (field, filter) => { const filterValue = convertV1DocumentField(filter.value); if (!filter.field.field_path) { throw new Error("field_path is required"); } switch (filter.op) { case v1StructuredQueryFieldFilterOperator.EQUAL: return field.eq(filterValue); case v1StructuredQueryFieldFilterOperator.LESS_THAN: return field.lt(filterValue); case v1StructuredQueryFieldFilterOperator.LESS_THAN_OR_EQUAL: return field.lte(filterValue); case v1StructuredQueryFieldFilterOperator.GREATER_THAN: return field.gt(filterValue); case v1StructuredQueryFieldFilterOperator.GREATER_THAN_OR_EQUAL: return field.gte(filterValue); case v1StructuredQueryFieldFilterOperator.ARRAY_CONTAINS: return field instanceof FirestoreStateDocumentArrayField && field.value.some((value) => value.eq(filterValue)); case v1StructuredQueryFieldFilterOperator.ARRAY_CONTAINS_ANY: return field instanceof FirestoreStateDocumentArrayField && filterValue instanceof FirestoreStateDocumentArrayField && field.value.some( (value) => filterValue.value.some((filterValue2) => value.eq(filterValue2)) ); case v1StructuredQueryFieldFilterOperator.IN: return filterValue instanceof FirestoreStateDocumentArrayField && filterValue.value.some((value) => field.eq(value)); case v1StructuredQueryFieldFilterOperator.NOT_EQUAL: return !field.eq(filterValue); case v1StructuredQueryFieldFilterOperator.NOT_IN: return filterValue instanceof FirestoreStateDocumentArrayField && filterValue.value.every((value) => !field.eq(value)); case v1StructuredQueryFieldFilterOperator.OPERATOR_UNSPECIFIED: { throw new Error( `Invalid query: op is not supported yet, ${v1StructuredQueryFieldFilterOperator[filter.op]}` ); } case void 0: throw new Error("Invalid query: op is required"); default: assertNever(filter.op); } }; var FirestoreState = class { constructor(projects = {}) { this.projects = projects; this.emitter = new EventEmitter(); } emitter; toJSON() { return { projects: Object.fromEntries( Object.entries(this.projects).map(([key, project]) => [ key, project.toJSON() ]) ) }; } getProject(projectName) { this.projects = produce2(this.projects, (draft) => { if (!(projectName in draft)) { const project2 = new FirestoreStateProject( this.emitter, projectName, {} ); draft[projectName] = project2; this.emitter.emit("add-project", { project: project2 }); } }); const project = this.projects[projectName]; if (!project) { throw new Error(`Project<${projectName}> not found.`); } return project; } getCollection(path) { const [ project, projectName, database, databaseName, documents, collectionName, ...rest ] = path.split("/"); if (project !== "projects" || typeof projectName !== "string" || database !== "databases" || typeof databaseName !== "string" || documents !== "documents" || typeof collectionName !== "string" || rest.length % 2 !== 0) { throw new Error(`Invalid path: ${path}`); } let current = this.getProject(projectName).getDatabase(databaseName).getCollection(collectionName); let next = rest; while (next.length > 0) { const [documentId, collectionName2, ...rest2] = next; if (typeof collectionName2 !== "string" || typeof documentId !== "string") { throw new Error(`Invalid path: ${path}`); } current = current.getDocument(documentId).getCollection(collectionName2); next = rest2; } return current; } getDocument(path) { const [docId, ...rest] = path.split("/").reverse(); const collection = this.getCollection(rest.slice().reverse().join("/")); if (typeof docId !== "string") { throw new Error(`Invalid path: ${path}`); } return collection.getDocument(docId); } clear() { this.projects = {}; this.emitter.emit("clear-all-projects", {}); } writeV1Document(date, write) { const writeTime = TimestampFromDate(date); if (write.has_delete) { const document = this.getDocument(write.delete); if (write.has_current_document && write.current_document.has_exists) { if (write.current_document.exists) { if (!document.metadata.hasExist) { throw new FirestoreEmulatorError( Status.NOT_FOUND, `no entity to update: app: "dev~${document.database.project.name}" path < ${document.iterateFromRoot().map( (doc) => ` Element { type: "${doc.parent.name}" name: "${doc.name}" }` ).join("\n")} > ` ); } } else { if (document.metadata.hasExist) { throw new FirestoreEmulatorError( Status.ALREADY_EXISTS, `entity already exists: EntityRef{partitionRef=dev~${document.database.project.name}, path=/${document.getDocumentPath()}}` ); } } } if (document.metadata.hasExist) { document.delete(); } return v1WriteResult.fromObject({ transform_results: [], update_time: writeTime }); } if (write.has_update) { if (write.update.has_create_time) { throw new Error("update.create_time is not implemented"); } if (write.update.has_update_time) { throw new Error("update.update_time is not implemented"); } const { name, fields } = write.update; const document = this.getDocument(name); if (write.has_current_document && write.current_document.has_exists) { if (!write.current_document.exists) { if (document.metadata.hasExist) { throw new FirestoreEmulatorError( Status.ALREADY_EXISTS, `entity already exists: EntityRef{partitionRef=dev~${document.database.project.name}, path=/${document.getDocumentPath()}}` ); } document.create( date, Object.fromEntries( Array.from(fields.entries()).map(([key, field]) => [ key, convertV1DocumentField(field) ]) ) ); document.v1Transform(date, write.update_transforms); } else { if (!document.metadata.hasExist) { throw new FirestoreEmulatorError( Status.NOT_FOUND, `no entity to update: app: "dev~${document.database.project.name}" path < ${document.iterateFromRoot().map( (doc) => ` Element { type: "${doc.parent.name}" name: "${doc.name}" }` ).join("\n")} > ` ); } document.update( date, Object.fromEntries( Array.from(fields.entries()).map(([key, field]) => [ key, convertV1DocumentField(field) ]) ), write.update_mask?.field_paths ); document.v1Transform(date, write.update_transforms); } } else { document.set( date, Object.fromEntries( Array.from(fields.entries()).map(([key, field]) => [ key, convertV1DocumentField(field) ]) ), write.update_mask?.field_paths ); document.v1Transform(date, write.update_transforms); } return v1WriteResult.fromObject({ transform_results: [], update_time: writeTime }); } throw new Error("Invalid write"); } v1Query(parent, query) { if (query.from.length !== 1) { throw new Error("query.from.length must be 1"); } const { collection_id } = query.from[0] ?? {}; if (!collection_id) { throw new Error("collection_id is not supported"); } const collectionName = `${parent}/${collection_id}`; const collection = this.getCollection(collectionName); let docs = collection.getAllDocuments().filter((v) => v.metadata.hasExist); docs = docs.slice().sort((aDocument, bDocument) => { for (const { field, direction } of query.order_by) { const a = aDocument.getField(field.field_path); const b = bDocument.getField(field.field_path); if (!a || !b) { if (a && !b) return -1; if (!a && b) return 1; continue; } if (a.eq(b)) continue; if (direction === v1StructuredQueryDirection.ASCENDING) { if (a.lt(b)) return -1; if (b.lt(a)) return 1; } else if (direction === v1StructuredQueryDirection.DESCENDING) { if (a.lt(b)) return 1; if (b.lt(a)) return -1; } } return 0; }); if (query.has_where) { if (query.where.has_field_filter) { const filter = query.where.field_filter; docs = docs.filter((document) => { if (!filter.field.field_path) { throw new Error("field_path is required"); } const field = document.getField(filter.field.field_path); if (!field) return false; return v1FilterDocuments(field, filter); }); } if (query.where.has_composite_filter) { switch (query.where.composite_filter.op) { case v1StructuredQueryCompositeFilterOperator.AND: case v1StructuredQueryCompositeFilterOperator.OR: break; case v1StructuredQueryCompositeFilterOperator.OPERATOR_UNSPECIFIED: throw new Error("Invalid query: op is required"); default: assertNever(query.where.composite_filter.op); } docs = docs.filter((document) => { if (query.where.composite_filter.op === v1StructuredQueryCompositeFilterOperator.AND) { return query.where.composite_filter.filters.every((filter) => { if (!filter.has_field_filter) { console.error("composite_filter only supports field_filter"); throw new Error("composite_filter only supports field_filter"); } const field = document.getField( filter.field_filter.field.field_path ); if (!field) return false; return v1FilterDocuments(field, filter.field_filter); }); } if (query.where.composite_filter.op === v1StructuredQueryCompositeFilterOperator.OR) { return query.where.composite_filter.filters.some((filter) => { if (!filter.has_field_filter) { console.error("composite_filter only supports field_filter"); throw new Error("composite_filter only supports field_filter"); } const field = document.getField( filter.field_filter.field.field_path ); if (!field) return false; return v1FilterDocuments(field, filter.field_filter); }); } return false; }); } } if (query.has_limit) { docs = docs.slice(0, query.limit.value); } return docs; } v1Listen(listen, callback, onEnd) { if (listen.has_remove_target) { console.error("remove_target is not implemented"); throw new Error("remove_target is not implemented"); } if (listen.has_add_target) { const sendNewDocuments = (docs) => { for (const doc of docs) { callback( v1ListenResponse.fromObject({ document_change: { document: doc.toV1DocumentObject(), target_ids: [listen.add_target.target_id] } }) ); } callback( v1ListenResponse.fromObject({ target_change: { read_time: TimestampFromNow().toObject(), target_change_type: v1TargetChangeTargetChangeType.CURRENT, target_ids: [listen.add_target.target_id] } }) ); callback( v1ListenResponse.fromObject({ target_change: { read_time: TimestampFromNow().toObject(), target_change_type: v1TargetChangeTargetChangeType.NO_CHANGE, target_ids: [] } }) ); }; if (listen.add_target.has_query) { let currentDocumentsPaths = []; callback( v1ListenResponse.fromObject({ target_change: { target_change_type: v1TargetChangeTargetChangeType.ADD, target_ids: [listen.add_target.target_id] } }) ); const docs = this.v1Query( listen.add_target.query.parent, listen.add_target.query.structured_query ); currentDocumentsPaths = docs.map((v) => v.getPath()); sendNewDocuments(docs); const handleOnUpdate = async () => { await new Promise((resolve) => setImmediate(resolve)); const nextDocuments = this.v1Query( listen.add_target.query.parent, listen.add_target.query.structured_query ); const newDocumentsPaths = nextDocuments.map((v) => v.getPath()); const hasCreate = newDocumentsPaths.some( (v) => !currentDocumentsPaths.includes(v) ); const hasDelete = currentDocumentsPaths.some( (v) => !newDocumentsPaths.includes(v) ); const hasUpdate = !hasCreate && !hasDelete && newDocumentsPaths.length === currentDocumentsPaths.length && newDocumentsPaths.every((v) => currentDocumentsPaths.includes(v)) && currentDocumentsPaths.every((v) => newDocumentsPaths.includes(v)); const hasChange = hasDelete || hasCreate || hasUpdate; if (!hasChange) { return; } callback( v1ListenResponse.fromObject({ target_change: { read_time: TimestampFromNow().toObject(), target_change_type: v1TargetChangeTargetChangeType.RESET, target_ids: [listen.add_target.target_id] } }) ); currentDocumentsPaths = nextDocuments.map((v) => v.getPath()); sendNewDocuments(nextDocuments); }; this.emitter.on("create-document", handleOnUpdate); onEnd(() => this.emitter.off("create-document", handleOnUpdate)); this.emitter.on("update-document", handleOnUpdate); onEnd(() => this.emitter.off("update-document", handleOnUpdate)); this.emitter.on("delete-document", handleOnUpdate); onEnd(() => this.emitter.off("delete-document", handleOnUpdate)); } else if (listen.add_target.has_documents) { callback( v1ListenResponse.fromObject({ target_change: { target_change_type: v1TargetChangeTargetChangeType.ADD, target_ids: [listen.add_target.target_id] } }) ); sendNewDocuments( listen.add_target.documents.documents.map((path) => { const document = this.getDocument(path); if (!document.metadata.hasExist) { return null; } return document; }).filter(isNotNull) ); const handleOnUpdate = async ({ document }) => { await new Promise((resolve) => setImmediate(resolve)); const hasChange = listen.add_target.documents.documents.includes( document.getPath() ); if (!hasChange) { return; } callback( v1ListenResponse.fromObject({ target_change: { read_time: TimestampFromNow().toObject(), target_change_type: v1TargetChangeTargetChangeType.RESET, target_ids: [listen.add_target.target_id] } }) ); sendNewDocuments( listen.add_target.documents.documents.map((path) => { const document2 = this.getDocument(path); if (!document2.metadata.hasExist) { return null; } return document2; }).filter(isNotNull) ); }; this.emitter.on("create-document", handleOnUpdate); onEnd(() => this.emitter.off("create-document", handleOnUpdate)); this.emitter.on("update-document", handleOnUpdate); onEnd(() => this.emitter.off("update-document", handleOnUpdate)); this.emitter.on("delete-document", handleOnUpdate); onEnd(() => this.emitter.off("delete-document", handleOnUpdate)); } } } }; // src/server/index.ts import { Server, ServerCredentials } from "@grpc/grpc-js"; // src/server/FirestoreServiceV1Impl/index.ts import { BatchGetDocumentsResponse, BeginTransactionResponse, CommitResponse, ListenRequest, RunAggregationQueryResponse, RunQueryResponse, UnimplementedFirestoreService } from "@firestore-emulator/proto/dist/google/firestore/v1/firestore"; import { Empty } from "@firestore-emulator/proto/dist/google/protobuf/empty"; var FirestoreServiceV1Impl = class extends UnimplementedFirestoreService { #state; constructor(state) { super(); this.#state = state; } GetDocument(_call, _callback) { console.error("Method<GetDocument> not implemented."); throw new Error("Method<GetDocument> not implemented."); } ListDocuments(_call, _callback) { console.error("Method<ListDocuments> not implemented."); throw new Error("Method<ListDocuments> not implemented."); } UpdateDocument(_call, _callback) { console.error("Method<UpdateDocument> not implemented."); throw new Error("Method<UpdateDocument> not implemented."); } DeleteDocument(_call, _callback) { console.error("Method<DeleteDocument> not implemented."); throw new Error("Method<DeleteDocument> not implemented."); } BatchGetDocuments(call) { const date = TimestampFromNow(); const tx = call.request.has_new_transaction ? Uint8Array.from([17, 2, 0, 0, 0, 0, 0, 0, 0]) : null; if (tx) { call.write( BatchGetDocumentsResponse.fromObject({ transaction: tx }) ); } for (const documentPath of call.request.documents) { const document = this.#state.getDocument(documentPath); call.write( BatchGetDocumentsResponse.fromObject({ found: document.metadata.hasExist ? document.toV1DocumentObject() : void 0, missing: document.metadata.hasExist ? void 0 : documentPath, read_time: date }) ); } call.end(); } BeginTransaction(_call, callback) { callback( null, BeginTransactionResponse.fromObject({ // dummy transaction id transaction: Uint8Array.from([17, 2, 0, 0, 0, 0, 0, 0, 0]) }) ); } Commit(call, callback) { const date = /* @__PURE__ */ new Date(); try { const results = call.request.writes.map((write) => { return this.#state.writeV1Document(date, write); }); callback( null, CommitResponse.fromObject({ commit_time: TimestampFromDate(date), write_results: results.map((result) => result.toObject()) }) ); } catch (err) { if (err instanceof FirestoreEmulatorError) { callback( { cause: err, code: err.code, message: err.message, name: "Error" }, null ); return; } console.error(err); callback( { cause: err, message: "Something went wrong", name: "Error" }, null ); } } Rollback(_call, callback) { callback(null, Empty.fromObject({})); } RunQuery(call) { const date = /* @__PURE__ */ new Date(); const results = this.#state.v1Query( call.request.parent, call.request.structured_query ); let transaction = call.request.transaction; if (call.request.consistency_selector === "new_transaction") { transaction = crypto.getRandomValues(new Uint8Array(12)); } if (results.length > 0) { results.forEach((result, i, arr) => { call.write( RunQueryResponse.fromObject({ document: result.toV1DocumentObject(), done: i === arr.length - 1, read_time: TimestampFromDate(date), skipped_results: 0, transaction }) ); }); } else { call.write( RunQueryResponse.fromObject({ done: true, read_time: TimestampFromDate(date), skipped_results: 0, transaction }) ); } call.end(); } RunAggregationQuery(call) { const date = TimestampFromNow(); const results = this.#state.v1Query( call.request.parent, call.request.structured_aggregation_query.structured_query ); let transaction = call.request.transaction; if (call.request.consistency_selector === "new_transaction") { transaction = crypto.getRandomValues(new Uint8Array(12)); } if (call.request.has_explain_options) { const error = new Error( "Explain for aggregation query is not supported." ); console.error(error); call.emit("error"); throw error; } const aggregateResult = Object.fromEntries( call.request.structured_aggregation_query.aggregations.map((agg) => { if (agg.has_count) return [agg.alias, { integer_value: results.length }]; if (agg.has_sum) { const sum = results.reduce((acc, cur) => { const field = cur.getField(agg.sum.field.field_path); if (!field) return acc;