@firestore-emulator/server
Version:
This package is the implementation of the Firestore emulator. It is a Node.js
1 lines • 87.1 kB
Source Map (JSON)
{"version":3,"sources":["../../src/FirestoreState/index.ts","../../src/error/error.ts","../../src/utils.ts","../../src/FirestoreState/field.ts","../../src/FirestoreState/mask.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\n\nimport { Document as v1Document } from \"@firestore-emulator/proto/dist/google/firestore/v1/document\";\nimport type { ListenRequest as v1ListenRequest } from \"@firestore-emulator/proto/dist/google/firestore/v1/firestore\";\nimport {\n ListenResponse as v1ListenResponse,\n TargetChangeTargetChangeType as v1TargetChangeTargetChangeType,\n} from \"@firestore-emulator/proto/dist/google/firestore/v1/firestore\";\nimport type {\n StructuredQuery as v1StructuredQuery,\n StructuredQueryFieldFilter as v1StructuredQueryFieldFilter,\n} from \"@firestore-emulator/proto/dist/google/firestore/v1/query\";\nimport {\n StructuredQueryCompositeFilterOperator as v1StructuredQueryCompositeFilterOperator,\n StructuredQueryDirection as v1StructuredQueryDirection,\n StructuredQueryFieldFilterOperator as v1StructuredQueryFieldFilterOperator,\n} from \"@firestore-emulator/proto/dist/google/firestore/v1/query\";\nimport type {\n DocumentTransformFieldTransform as v1DocumentTransformFieldTransform,\n Write as v1Write,\n} from \"@firestore-emulator/proto/dist/google/firestore/v1/write\";\nimport {\n DocumentTransformFieldTransformServerValue,\n WriteResult as v1WriteResult,\n} from \"@firestore-emulator/proto/dist/google/firestore/v1/write\";\nimport { Timestamp } from \"@firestore-emulator/proto/dist/google/protobuf/timestamp\";\nimport { Status } from \"@grpc/grpc-js/build/src/constants\";\nimport { assertNever } from \"assert-never\";\nimport { produce } from \"immer\";\nimport type { TypeSafeEventEmitter } from \"typesafe-event-emitter\";\n\nimport { FirestoreEmulatorError } from \"../error/error\";\nimport { isNotNull } from \"../utils\";\n\nimport type { FirestoreStateDocumentFields } from \"./field\";\nimport {\n FirestoreStateDocumentArrayField,\n FirestoreStateDocumentDoubleField,\n FirestoreStateDocumentIntegerField,\n FirestoreStateDocumentTimestampField,\n convertV1DocumentField,\n convertV1Value,\n} from \"./field\";\nimport { updateFields } from \"./mask\";\n\ninterface Events {\n \"add-collection\": { collection: FirestoreStateCollection };\n \"add-database\": { database: FirestoreStateDatabase };\n \"add-document\": { document: FirestoreStateDocument };\n \"add-project\": { project: FirestoreStateProject };\n \"clear-all-projects\": Record<string, never>;\n \"create-document\": { document: FirestoreStateDocument };\n \"delete-document\": { document: FirestoreStateDocument };\n \"update-document\": { document: FirestoreStateDocument };\n}\n\nexport interface HasCollections {\n getCollection(collectionName: string): FirestoreStateCollection;\n getPath(): string;\n}\n\ntype FirestoreStateDocumentMetadata =\n | {\n createdAt: null;\n hasExist: false;\n updatedAt: null;\n }\n | {\n createdAt: Date;\n hasExist: true;\n updatedAt: Date;\n };\n\nexport class FirestoreStateDocument implements HasCollections {\n metadata: FirestoreStateDocumentMetadata = {\n createdAt: null,\n hasExist: false,\n updatedAt: null,\n };\n constructor(\n private emitter: TypeSafeEventEmitter<Events>,\n readonly database: FirestoreStateDatabase,\n readonly parent: FirestoreStateCollection,\n readonly name: string,\n private fields: Record<string, FirestoreStateDocumentFields>,\n private collections: Record<string, FirestoreStateCollection>,\n ) {}\n\n iterateFromRoot() {\n const iters: FirestoreStateDocument[] = [this];\n let current: FirestoreStateDocument = this;\n while (current.parent.parent instanceof FirestoreStateDocument) {\n const next = current.parent.parent;\n current = next;\n iters.push(current);\n }\n return iters.slice().reverse();\n }\n\n hasChild(): boolean {\n for (const collection of Object.values(this.collections)) {\n if (collection.hasChild()) return true;\n }\n return false;\n }\n\n getCollection(collectionName: string) {\n this.collections = produce(this.collections, (draft) => {\n if (!(collectionName in draft)) {\n const collection = new FirestoreStateCollection(\n this.emitter,\n this.database,\n this,\n collectionName,\n {},\n );\n draft[collectionName] = collection;\n this.emitter.emit(\"add-collection\", { collection });\n }\n });\n\n const collection = this.collections[collectionName];\n if (!collection) {\n throw new Error(`Collection<${collectionName}> not found.`);\n }\n return collection;\n }\n\n toV1Document(): v1Document {\n return v1Document.fromObject({\n create_time: this.metadata.hasExist\n ? Timestamp.fromObject({\n nanos: this.metadata.createdAt.getMilliseconds() * 1000 * 1000,\n seconds: Math.floor(this.metadata.createdAt.getTime() / 1000),\n })\n : undefined,\n fields: Object.fromEntries(\n Object.entries(this.fields).map(([key, field]) => [\n key,\n field.toV1ValueObject(),\n ]),\n ),\n name: this.getPath(),\n update_time: this.metadata.hasExist\n ? Timestamp.fromObject({\n nanos: this.metadata.updatedAt.getMilliseconds() * 1000 * 1000,\n seconds: Math.floor(this.metadata.updatedAt.getTime() / 1000),\n })\n : undefined,\n });\n }\n\n toV1DocumentObject(): ReturnType<typeof v1Document.prototype.toObject> {\n const document = this.toV1Document();\n return {\n create_time: document.create_time.toObject(),\n fields: Object.fromEntries(\n Array.from(document.fields).map(([key, value]) => [\n key,\n convertV1Value(value),\n ]),\n ),\n name: document.name,\n update_time: document.update_time.toObject(),\n };\n }\n\n toJSON(): {\n collections: Record<string, ReturnType<FirestoreStateCollection[\"toJSON\"]>>;\n fields: Record<string, ReturnType<FirestoreStateDocumentFields[\"toJSON\"]>>;\n path: string;\n } {\n return {\n collections: Object.fromEntries(\n Object.entries(this.collections)\n .filter(([, collection]) => collection.hasChild())\n .map(([key, collection]) => [key, collection.toJSON()]),\n ),\n fields: Object.fromEntries(\n Object.entries(this.fields).map(([key, field]) => [\n key,\n field.toJSON(),\n ]),\n ),\n path: this.getPath(),\n };\n }\n\n create(date: Date, fields: Record<string, FirestoreStateDocumentFields>) {\n if (this.metadata.hasExist) {\n throw new Error(\"Document already exists.\");\n }\n this.metadata = produce<\n FirestoreStateDocumentMetadata,\n FirestoreStateDocumentMetadata\n >(this.metadata, (draft) => {\n draft.hasExist = true;\n draft.createdAt = date;\n draft.updatedAt = date;\n });\n this.fields = produce(this.fields, (draft) => {\n for (const [key, field] of Object.entries(fields)) {\n draft[key] = field;\n }\n });\n this.emitter.emit(\"create-document\", { document: this });\n }\n\n update(\n date: Date,\n fields: Record<string, FirestoreStateDocumentFields>,\n updateMask: string[] = [],\n ) {\n if (!this.metadata.hasExist) {\n throw new Error(\"Document does not exist.\");\n }\n this.metadata = produce<\n FirestoreStateDocumentMetadata,\n FirestoreStateDocumentMetadata\n >(this.metadata, (draft) => {\n draft.updatedAt = date;\n });\n this.fields = updateFields(this.fields, fields, updateMask);\n this.emitter.emit(\"update-document\", { document: this });\n }\n\n set(\n date: Date,\n fields: Record<string, FirestoreStateDocumentFields>,\n updateMask: string[] = [],\n ) {\n const isCreate = !this.metadata.hasExist;\n if (!this.metadata.hasExist) {\n this.metadata = produce<\n FirestoreStateDocumentMetadata,\n FirestoreStateDocumentMetadata\n >(this.metadata, (draft) => {\n draft.hasExist = true;\n draft.createdAt = date;\n draft.updatedAt = date;\n });\n } else {\n this.metadata = produce<\n FirestoreStateDocumentMetadata,\n FirestoreStateDocumentMetadata\n >(this.metadata, (draft) => {\n draft.updatedAt = date;\n });\n }\n\n this.fields = updateFields(this.fields, fields, updateMask);\n if (isCreate) {\n this.emitter.emit(\"create-document\", { document: this });\n } else {\n this.emitter.emit(\"update-document\", { document: this });\n }\n }\n\n delete() {\n if (!this.metadata.hasExist) {\n throw new Error(\"Document does not exist.\");\n }\n this.metadata = produce<\n FirestoreStateDocumentMetadata,\n FirestoreStateDocumentMetadata\n >(this.metadata, (draft) => {\n draft.hasExist = false;\n draft.createdAt = null;\n draft.updatedAt = null;\n });\n this.fields = produce(this.fields, (draft) => {\n for (const key of Object.keys(draft)) {\n delete draft[key];\n }\n });\n this.emitter.emit(\"delete-document\", { document: this });\n }\n\n getField(path: string) {\n if (path.includes(\".\")) {\n throw new Error(\"Not implemented\");\n }\n return this.fields[path];\n }\n\n getPath(): string {\n return `${this.parent.getPath()}/${this.name}`;\n }\n\n getDocumentPath(): string {\n return `${this.parent.getDocumentPath()}/${this.name}`;\n }\n\n v1Transform(date: Date, transforms: v1DocumentTransformFieldTransform[]) {\n for (const transform of transforms) {\n const field = this.getField(transform.field_path);\n if (transform.has_increment) {\n if (transform.increment.has_integer_value) {\n if (!field) {\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentIntegerField(\n transform.increment.integer_value,\n ),\n });\n continue;\n }\n if (field instanceof FirestoreStateDocumentIntegerField) {\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentIntegerField(\n field.value + transform.increment.integer_value,\n ),\n });\n continue;\n }\n if (field instanceof FirestoreStateDocumentDoubleField) {\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentDoubleField(\n field.value + transform.increment.integer_value,\n ),\n });\n continue;\n }\n throw new Error(\n `Invalid transform: ${JSON.stringify(\n transform,\n )}. increment transform can only be applied to an integer or a double field`,\n );\n }\n if (transform.increment.has_double_value) {\n if (!field) {\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentDoubleField(\n transform.increment.double_value,\n ),\n });\n continue;\n }\n if (\n field instanceof FirestoreStateDocumentIntegerField ||\n field instanceof FirestoreStateDocumentDoubleField\n ) {\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentDoubleField(\n field.value + transform.increment.double_value,\n ),\n });\n continue;\n }\n throw new Error(\n `Invalid transform: ${JSON.stringify(\n transform,\n )}. increment transform can only be applied to an integer or a double field`,\n );\n }\n throw new Error(\n `Invalid transform: ${JSON.stringify(\n transform,\n )}. increment transform can only be applied to an integer or a double field`,\n );\n }\n if (transform.has_remove_all_from_array) {\n const removeFields = transform.remove_all_from_array.values.map(\n convertV1DocumentField,\n );\n if (field instanceof FirestoreStateDocumentArrayField) {\n const removedFields = field.value.filter(\n (value) =>\n !removeFields.some((removeField) => removeField.eq(value)),\n );\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentArrayField(\n removedFields,\n ),\n });\n return;\n }\n }\n if (transform.has_append_missing_elements) {\n const appendFields = transform.append_missing_elements.values.map(\n convertV1DocumentField,\n );\n if (field instanceof FirestoreStateDocumentArrayField) {\n const appendedFields = [\n ...field.value,\n ...appendFields.filter(\n (appendField) =>\n !field.value.some((value) => value.eq(appendField)),\n ),\n ];\n this.set(date, {\n [transform.field_path]: new FirestoreStateDocumentArrayField(\n appendedFields,\n ),\n });\n return;\n }\n }\n if (transform.has_set_to_server_value) {\n if (\n transform.set_to_server_value ===\n DocumentTransformFieldTransformServerValue.REQUEST_TIME\n ) {\n this.set(date, {\n [transform.field_path]:\n FirestoreStateDocumentTimestampField.fromDate(date),\n });\n return;\n }\n throw new Error(\n `Invalid transform: ${JSON.stringify(\n transform,\n )}. set_to_server_value must be a valid value`,\n );\n }\n throw new Error(\n `Invalid transform: ${JSON.stringify(transform.toObject(), null, 4)}`,\n );\n }\n }\n}\n\nexport class FirestoreStateCollection {\n constructor(\n private emitter: TypeSafeEventEmitter<Events>,\n readonly database: FirestoreStateDatabase,\n readonly parent: FirestoreStateDocument | FirestoreStateDatabase,\n readonly name: string,\n private documents: Record<string, FirestoreStateDocument>,\n ) {}\n\n getDocument(documentName: string) {\n this.documents = produce(this.documents, (draft) => {\n if (!(documentName in draft)) {\n const document = new FirestoreStateDocument(\n this.emitter,\n this.database,\n this,\n documentName,\n {},\n {},\n );\n draft[documentName] = document;\n }\n });\n\n const document = this.documents[documentName];\n if (!document) {\n throw new Error(`Document<${documentName}> not found.`);\n }\n return document;\n }\n\n hasChild(): boolean {\n for (const document of Object.values(this.documents)) {\n if (document.metadata.hasExist) return true;\n if (document.hasChild()) return true;\n }\n return false;\n }\n\n getAllDocuments() {\n return Object.values(this.documents);\n }\n\n toJSON() {\n return {\n documents: Object.fromEntries(\n Object.entries(this.documents)\n .filter(\n ([, document]) => document.metadata.hasExist || document.hasChild(),\n )\n .map(([key, document]) => [key, document.toJSON()]),\n ),\n path: this.getPath(),\n };\n }\n\n getPath(): string {\n return `${this.parent.getPath()}/${this.name}`;\n }\n\n getDocumentPath(): string {\n if (this.parent instanceof FirestoreStateDatabase) return this.name;\n return `${this.parent.getDocumentPath()}/${this.name}`;\n }\n}\n\nexport class FirestoreStateDatabase implements HasCollections {\n constructor(\n private emitter: TypeSafeEventEmitter<Events>,\n readonly project: FirestoreStateProject,\n readonly name: string,\n private collections: Record<string, FirestoreStateCollection>,\n ) {}\n\n getCollection(collectionName: string) {\n this.collections = produce(this.collections, (draft) => {\n if (!(collectionName in draft)) {\n const collection = new FirestoreStateCollection(\n this.emitter,\n this,\n this,\n collectionName,\n {},\n );\n draft[collectionName] = collection;\n this.emitter.emit(\"add-collection\", { collection });\n }\n });\n\n const collection = this.collections[collectionName];\n if (!collection) {\n throw new Error(`Collection<${collectionName}> not found.`);\n }\n return collection;\n }\n\n toJSON() {\n return {\n collections: Object.fromEntries(\n Object.entries(this.collections).map(([key, collection]) => [\n key,\n collection.toJSON(),\n ]),\n ),\n path: this.getPath(),\n };\n }\n\n getPath(): string {\n return `${this.project.getPath()}/databases/${this.name}/documents`;\n }\n}\n\nexport class FirestoreStateProject {\n constructor(\n private emitter: TypeSafeEventEmitter<Events>,\n readonly name: string,\n private databases: Record<string, FirestoreStateDatabase>,\n ) {}\n\n toJSON() {\n return {\n databases: Object.fromEntries(\n Object.entries(this.databases).map(([key, database]) => [\n key,\n database.toJSON(),\n ]),\n ),\n path: this.getPath(),\n };\n }\n\n getPath(): string {\n return `projects/${this.name}`;\n }\n\n getDatabase(databaseName: string) {\n this.databases = produce(this.databases, (draft) => {\n if (!(databaseName in draft)) {\n const database = new FirestoreStateDatabase(\n this.emitter,\n this,\n databaseName,\n {},\n );\n draft[databaseName] = database;\n this.emitter.emit(\"add-database\", { database });\n }\n });\n\n const database = this.databases[databaseName];\n if (!database) {\n throw new Error(`Database<${databaseName}> not found.`);\n }\n return database;\n }\n}\n\nexport const TimestampFromDate = (date: Date) =>\n Timestamp.fromObject({\n nanos: (date.getTime() % 1000) * 1000 * 1000,\n seconds: Math.floor(date.getTime() / 1000),\n });\nexport const DateFromTimestamp = (timestamp: Timestamp) =>\n new Date(timestamp.seconds * 1000 + timestamp.nanos / 1000 / 1000);\n\nexport const TimestampFromNow = () => TimestampFromDate(new Date());\n\nconst v1FilterDocuments = (\n field: FirestoreStateDocumentFields,\n filter: v1StructuredQueryFieldFilter,\n) => {\n const filterValue = convertV1DocumentField(filter.value);\n if (!filter.field.field_path) {\n throw new Error(\"field_path is required\");\n }\n switch (filter.op) {\n case v1StructuredQueryFieldFilterOperator.EQUAL:\n return field.eq(filterValue);\n case v1StructuredQueryFieldFilterOperator.LESS_THAN:\n return field.lt(filterValue);\n case v1StructuredQueryFieldFilterOperator.LESS_THAN_OR_EQUAL:\n return field.lte(filterValue);\n case v1StructuredQueryFieldFilterOperator.GREATER_THAN:\n return field.gt(filterValue);\n case v1StructuredQueryFieldFilterOperator.GREATER_THAN_OR_EQUAL:\n return field.gte(filterValue);\n case v1StructuredQueryFieldFilterOperator.ARRAY_CONTAINS:\n return (\n field instanceof FirestoreStateDocumentArrayField &&\n field.value.some((value) => value.eq(filterValue))\n );\n case v1StructuredQueryFieldFilterOperator.ARRAY_CONTAINS_ANY:\n return (\n field instanceof FirestoreStateDocumentArrayField &&\n filterValue instanceof FirestoreStateDocumentArrayField &&\n field.value.some((value) =>\n filterValue.value.some((filterValue) => value.eq(filterValue)),\n )\n );\n case v1StructuredQueryFieldFilterOperator.IN:\n return (\n filterValue instanceof FirestoreStateDocumentArrayField &&\n filterValue.value.some((value) => field.eq(value))\n );\n case v1StructuredQueryFieldFilterOperator.NOT_EQUAL:\n return !field.eq(filterValue);\n case v1StructuredQueryFieldFilterOperator.NOT_IN:\n return (\n filterValue instanceof FirestoreStateDocumentArrayField &&\n filterValue.value.every((value) => !field.eq(value))\n );\n case v1StructuredQueryFieldFilterOperator.OPERATOR_UNSPECIFIED: {\n throw new Error(\n `Invalid query: op is not supported yet, ${\n v1StructuredQueryFieldFilterOperator[filter.op]\n }`,\n );\n }\n case undefined:\n throw new Error(\"Invalid query: op is required\");\n default:\n assertNever(filter.op);\n }\n};\n\nexport class FirestoreState {\n readonly emitter: TypeSafeEventEmitter<Events>;\n constructor(private projects: Record<string, FirestoreStateProject> = {}) {\n this.emitter = new EventEmitter();\n }\n\n toJSON() {\n return {\n projects: Object.fromEntries(\n Object.entries(this.projects).map(([key, project]) => [\n key,\n project.toJSON(),\n ]),\n ),\n };\n }\n\n getProject(projectName: string) {\n this.projects = produce(this.projects, (draft) => {\n if (!(projectName in draft)) {\n const project = new FirestoreStateProject(\n this.emitter,\n projectName,\n {},\n );\n draft[projectName] = project;\n this.emitter.emit(\"add-project\", { project });\n }\n });\n\n const project = this.projects[projectName];\n if (!project) {\n throw new Error(`Project<${projectName}> not found.`);\n }\n return project;\n }\n\n getCollection(path: string) {\n const [\n project,\n projectName,\n database,\n databaseName,\n documents,\n collectionName,\n ...rest\n ] = path.split(\"/\");\n if (\n project !== \"projects\" ||\n typeof projectName !== \"string\" ||\n database !== \"databases\" ||\n typeof databaseName !== \"string\" ||\n documents !== \"documents\" ||\n typeof collectionName !== \"string\" ||\n rest.length % 2 !== 0\n ) {\n throw new Error(`Invalid path: ${path}`);\n }\n\n let current = this.getProject(projectName)\n .getDatabase(databaseName)\n .getCollection(collectionName);\n let next = rest;\n while (next.length > 0) {\n const [documentId, collectionName, ...rest] = next;\n if (\n typeof collectionName !== \"string\" ||\n typeof documentId !== \"string\"\n ) {\n throw new Error(`Invalid path: ${path}`);\n }\n current = current.getDocument(documentId).getCollection(collectionName);\n next = rest;\n }\n return current;\n }\n\n getDocument(path: string) {\n const [docId, ...rest] = path.split(\"/\").reverse();\n const collection = this.getCollection(rest.slice().reverse().join(\"/\"));\n\n if (typeof docId !== \"string\") {\n throw new Error(`Invalid path: ${path}`);\n }\n return collection.getDocument(docId);\n }\n\n clear() {\n this.projects = {};\n this.emitter.emit(\"clear-all-projects\", {});\n }\n\n writeV1Document(date: Date, write: v1Write): v1WriteResult {\n const writeTime = TimestampFromDate(date);\n if (write.has_delete) {\n const document = this.getDocument(write.delete);\n if (write.has_current_document && write.current_document.has_exists) {\n // 存在確認を行う\n if (write.current_document.exists) {\n if (!document.metadata.hasExist) {\n throw new FirestoreEmulatorError(\n Status.NOT_FOUND,\n `no entity to update: app: \"dev~${document.database.project.name}\"\npath <\n${document\n .iterateFromRoot()\n .map(\n (doc) =>\n ` Element {\n type: \"${doc.parent.name}\"\n name: \"${doc.name}\"\n }`,\n )\n .join(\"\\n\")}\n>\n`,\n );\n }\n } else {\n if (document.metadata.hasExist) {\n throw new FirestoreEmulatorError(\n Status.ALREADY_EXISTS,\n `entity already exists: EntityRef{partitionRef=dev~${\n document.database.project.name\n }, path=/${document.getDocumentPath()}}`,\n );\n }\n }\n }\n if (document.metadata.hasExist) {\n document.delete();\n }\n return v1WriteResult.fromObject({\n transform_results: [],\n update_time: writeTime,\n });\n }\n if (write.has_update) {\n if (write.update.has_create_time) {\n throw new Error(\"update.create_time is not implemented\");\n }\n if (write.update.has_update_time) {\n throw new Error(\"update.update_time is not implemented\");\n }\n const { name, fields } = write.update;\n const document = this.getDocument(name);\n if (write.has_current_document && write.current_document.has_exists) {\n if (!write.current_document.exists) {\n if (document.metadata.hasExist) {\n throw new FirestoreEmulatorError(\n Status.ALREADY_EXISTS,\n `entity already exists: EntityRef{partitionRef=dev~${\n document.database.project.name\n }, path=/${document.getDocumentPath()}}`,\n );\n }\n\n document.create(\n date,\n Object.fromEntries(\n Array.from(fields.entries()).map(([key, field]) => [\n key,\n convertV1DocumentField(field),\n ]),\n ),\n );\n document.v1Transform(date, write.update_transforms);\n } else {\n if (!document.metadata.hasExist) {\n throw new FirestoreEmulatorError(\n Status.NOT_FOUND,\n `no entity to update: app: \"dev~${document.database.project.name}\"\npath <\n${document\n .iterateFromRoot()\n .map(\n (doc) =>\n ` Element {\n type: \"${doc.parent.name}\"\n name: \"${doc.name}\"\n }`,\n )\n .join(\"\\n\")}\n>\n`,\n );\n }\n\n document.update(\n date,\n Object.fromEntries(\n Array.from(fields.entries()).map(([key, field]) => [\n key,\n convertV1DocumentField(field),\n ]),\n ),\n write.update_mask?.field_paths,\n );\n document.v1Transform(date, write.update_transforms);\n }\n } else {\n document.set(\n date,\n Object.fromEntries(\n Array.from(fields.entries()).map(([key, field]) => [\n key,\n convertV1DocumentField(field),\n ]),\n ),\n write.update_mask?.field_paths,\n );\n document.v1Transform(date, write.update_transforms);\n }\n\n return v1WriteResult.fromObject({\n transform_results: [],\n update_time: writeTime,\n });\n }\n throw new Error(\"Invalid write\");\n }\n\n v1Query(parent: string, query: v1StructuredQuery) {\n if (query.from.length !== 1) {\n throw new Error(\"query.from.length must be 1\");\n }\n const { collection_id } = query.from[0] ?? {};\n if (!collection_id) {\n throw new Error(\"collection_id is not supported\");\n }\n const collectionName = `${parent}/${collection_id}`;\n const collection = this.getCollection(collectionName);\n let docs = collection.getAllDocuments().filter((v) => v.metadata.hasExist);\n\n docs = docs.slice().sort((aDocument, bDocument) => {\n for (const { field, direction } of query.order_by) {\n const a = aDocument.getField(field.field_path);\n const b = bDocument.getField(field.field_path);\n if (!a || !b) {\n if (a && !b) return -1;\n if (!a && b) return 1;\n continue;\n }\n if (a.eq(b)) continue;\n if (direction === v1StructuredQueryDirection.ASCENDING) {\n if (a.lt(b)) return -1;\n if (b.lt(a)) return 1;\n } else if (direction === v1StructuredQueryDirection.DESCENDING) {\n if (a.lt(b)) return 1;\n if (b.lt(a)) return -1;\n }\n }\n return 0;\n });\n\n if (query.has_where) {\n if (query.where.has_field_filter) {\n const filter = query.where.field_filter;\n\n docs = docs.filter((document) => {\n if (!filter.field.field_path) {\n throw new Error(\"field_path is required\");\n }\n const field = document.getField(filter.field.field_path);\n if (!field) return false;\n return v1FilterDocuments(field, filter);\n });\n }\n if (query.where.has_composite_filter) {\n switch (query.where.composite_filter.op) {\n case v1StructuredQueryCompositeFilterOperator.AND:\n case v1StructuredQueryCompositeFilterOperator.OR:\n break;\n case v1StructuredQueryCompositeFilterOperator.OPERATOR_UNSPECIFIED:\n throw new Error(\"Invalid query: op is required\");\n default:\n assertNever(query.where.composite_filter.op);\n }\n\n docs = docs.filter((document) => {\n if (\n query.where.composite_filter.op ===\n v1StructuredQueryCompositeFilterOperator.AND\n ) {\n return query.where.composite_filter.filters.every((filter) => {\n if (!filter.has_field_filter) {\n console.error(\"composite_filter only supports field_filter\");\n throw new Error(\"composite_filter only supports field_filter\");\n }\n const field = document.getField(\n filter.field_filter.field.field_path,\n );\n if (!field) return false;\n return v1FilterDocuments(field, filter.field_filter);\n });\n }\n if (\n query.where.composite_filter.op ===\n v1StructuredQueryCompositeFilterOperator.OR\n ) {\n return query.where.composite_filter.filters.some((filter) => {\n if (!filter.has_field_filter) {\n console.error(\"composite_filter only supports field_filter\");\n throw new Error(\"composite_filter only supports field_filter\");\n }\n const field = document.getField(\n filter.field_filter.field.field_path,\n );\n if (!field) return false;\n return v1FilterDocuments(field, filter.field_filter);\n });\n }\n // this is unreachable\n return false;\n });\n }\n }\n\n if (query.has_limit) {\n docs = docs.slice(0, query.limit.value);\n }\n\n return docs;\n }\n\n v1Listen(\n listen: v1ListenRequest,\n callback: (response: v1ListenResponse) => void,\n onEnd: (handler: () => void) => void,\n ) {\n if (listen.has_remove_target) {\n console.error(\"remove_target is not implemented\");\n throw new Error(\"remove_target is not implemented\");\n }\n if (listen.has_add_target) {\n const sendNewDocuments = (docs: FirestoreStateDocument[]) => {\n for (const doc of docs) {\n callback(\n v1ListenResponse.fromObject({\n document_change: {\n document: doc.toV1DocumentObject(),\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n }\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n read_time: TimestampFromNow().toObject(),\n target_change_type: v1TargetChangeTargetChangeType.CURRENT,\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n read_time: TimestampFromNow().toObject(),\n target_change_type: v1TargetChangeTargetChangeType.NO_CHANGE,\n target_ids: [],\n },\n }),\n );\n };\n\n if (listen.add_target.has_query) {\n let currentDocumentsPaths: string[] = [];\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n target_change_type: v1TargetChangeTargetChangeType.ADD,\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n\n const docs = this.v1Query(\n listen.add_target.query.parent,\n listen.add_target.query.structured_query,\n );\n currentDocumentsPaths = docs.map((v) => v.getPath());\n sendNewDocuments(docs);\n\n const handleOnUpdate = async () => {\n await new Promise((resolve) => setImmediate(resolve));\n const nextDocuments = this.v1Query(\n listen.add_target.query.parent,\n listen.add_target.query.structured_query,\n );\n const newDocumentsPaths = nextDocuments.map((v) => v.getPath());\n const hasCreate = newDocumentsPaths.some(\n (v) => !currentDocumentsPaths.includes(v),\n );\n const hasDelete = currentDocumentsPaths.some(\n (v) => !newDocumentsPaths.includes(v),\n );\n const hasUpdate =\n !hasCreate &&\n !hasDelete &&\n newDocumentsPaths.length === currentDocumentsPaths.length &&\n newDocumentsPaths.every((v) => currentDocumentsPaths.includes(v)) &&\n currentDocumentsPaths.every((v) => newDocumentsPaths.includes(v));\n\n const hasChange = hasDelete || hasCreate || hasUpdate;\n\n if (!hasChange) {\n // pass\n return;\n }\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n read_time: TimestampFromNow().toObject(),\n target_change_type: v1TargetChangeTargetChangeType.RESET,\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n currentDocumentsPaths = nextDocuments.map((v) => v.getPath());\n sendNewDocuments(nextDocuments);\n };\n this.emitter.on(\"create-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"create-document\", handleOnUpdate));\n\n this.emitter.on(\"update-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"update-document\", handleOnUpdate));\n this.emitter.on(\"delete-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"delete-document\", handleOnUpdate));\n } else if (listen.add_target.has_documents) {\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n target_change_type: v1TargetChangeTargetChangeType.ADD,\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n\n sendNewDocuments(\n listen.add_target.documents.documents\n .map((path) => {\n const document = this.getDocument(path);\n if (!document.metadata.hasExist) {\n return null;\n }\n return document;\n })\n .filter(isNotNull),\n );\n\n const handleOnUpdate = async ({\n document,\n }: {\n document: FirestoreStateDocument;\n }) => {\n await new Promise((resolve) => setImmediate(resolve));\n const hasChange = listen.add_target.documents.documents.includes(\n document.getPath(),\n );\n if (!hasChange) {\n // pass\n return;\n }\n callback(\n v1ListenResponse.fromObject({\n target_change: {\n read_time: TimestampFromNow().toObject(),\n target_change_type: v1TargetChangeTargetChangeType.RESET,\n target_ids: [listen.add_target.target_id],\n },\n }),\n );\n sendNewDocuments(\n listen.add_target.documents.documents\n .map((path) => {\n const document = this.getDocument(path);\n if (!document.metadata.hasExist) {\n return null;\n }\n return document;\n })\n .filter(isNotNull),\n );\n };\n this.emitter.on(\"create-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"create-document\", handleOnUpdate));\n\n this.emitter.on(\"update-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"update-document\", handleOnUpdate));\n this.emitter.on(\"delete-document\", handleOnUpdate);\n onEnd(() => this.emitter.off(\"delete-document\", handleOnUpdate));\n }\n }\n }\n}\n","import type { Status } from \"@grpc/grpc-js/build/src/constants\";\n\nexport class FirestoreEmulatorError extends Error {\n constructor(\n readonly code: Status,\n message: string,\n ) {\n super(message);\n }\n}\n","export const isNotNull = <T>(value: T): value is NonNullable<T> =>\n value != null;\n","import type { Value as v1Value } from \"@firestore-emulator/proto/dist/google/firestore/v1/document\";\nimport { NullValue } from \"@firestore-emulator/proto/dist/google/protobuf/struct\";\n\nexport type ValueObjectType = ReturnType<typeof v1Value.prototype.toObject>;\nexport interface FirestoreStateDocumentBaseField {\n eq(other: FirestoreStateDocumentFields): boolean;\n gt(other: FirestoreStateDocumentFields): boolean;\n gte(other: FirestoreStateDocumentFields): boolean;\n lt(other: FirestoreStateDocumentFields): boolean;\n lte(other: FirestoreStateDocumentFields): boolean;\n toJSON(): { type: string; value: unknown };\n toV1ValueObject(): ValueObjectType;\n}\n\nexport class FirestoreStateDocumentStringField\n implements FirestoreStateDocumentBaseField\n{\n type = \"string_value\" as const;\n constructor(readonly value: string) {}\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { string_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentStringField &&\n this.value === other.value\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentStringField &&\n this.value < other.value\n );\n }\n\n lte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentStringField &&\n this.value <= other.value\n );\n }\n\n gt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentStringField &&\n this.value > other.value\n );\n }\n\n gte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentStringField &&\n this.value >= other.value\n );\n }\n}\n\nexport class FirestoreStateDocumentNullField\n implements FirestoreStateDocumentBaseField\n{\n type = \"null_value\" as const;\n value = null;\n toJSON() {\n return { type: this.type, value: null } as const;\n }\n\n toV1ValueObject(): ValueObjectType {\n return { null_value: NullValue.NULL_VALUE };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return other.type === this.type;\n }\n\n lt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n lte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n}\n\nexport class FirestoreStateDocumentBooleanField\n implements FirestoreStateDocumentBaseField\n{\n type = \"boolean_value\" as const;\n constructor(readonly value: boolean) {}\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { boolean_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBooleanField &&\n this.value === other.value\n );\n }\n\n lt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n lte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n}\n\nexport class FirestoreStateDocumentIntegerField\n implements FirestoreStateDocumentBaseField\n{\n type = \"integer_value\" as const;\n constructor(readonly value: number) {\n if (!Number.isInteger(value)) {\n throw new Error(`value must be integer. value=${value}`);\n }\n }\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { integer_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentIntegerField &&\n this.value === other.value\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentIntegerField &&\n this.value < other.value\n );\n }\n\n lte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentIntegerField &&\n this.value <= other.value\n );\n }\n\n gt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentIntegerField &&\n this.value > other.value\n );\n }\n\n gte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentIntegerField &&\n this.value >= other.value\n );\n }\n\n add(\n other: FirestoreStateDocumentIntegerField,\n ): FirestoreStateDocumentIntegerField;\n add(\n other: FirestoreStateDocumentDoubleField,\n ): FirestoreStateDocumentDoubleField;\n add(\n other:\n | FirestoreStateDocumentIntegerField\n | FirestoreStateDocumentDoubleField,\n ): FirestoreStateDocumentIntegerField | FirestoreStateDocumentDoubleField {\n if (other instanceof FirestoreStateDocumentIntegerField) {\n return new FirestoreStateDocumentIntegerField(this.value + other.value);\n }\n if (other instanceof FirestoreStateDocumentDoubleField) {\n return new FirestoreStateDocumentDoubleField(this.value + other.value);\n }\n throw new Error(`unsupported type. other=${other}`);\n }\n}\n\nexport class FirestoreStateDocumentDoubleField\n implements FirestoreStateDocumentBaseField\n{\n type = \"double_value\" as const;\n constructor(readonly value: number) {}\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { double_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentDoubleField &&\n this.value === other.value\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentDoubleField &&\n this.value < other.value\n );\n }\n\n lte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentDoubleField &&\n this.value <= other.value\n );\n }\n\n gt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentDoubleField &&\n this.value > other.value\n );\n }\n\n gte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentDoubleField &&\n this.value >= other.value\n );\n }\n\n add(\n other:\n | FirestoreStateDocumentIntegerField\n | FirestoreStateDocumentDoubleField,\n ): FirestoreStateDocumentDoubleField {\n if (other instanceof FirestoreStateDocumentIntegerField) {\n return new FirestoreStateDocumentDoubleField(this.value + other.value);\n }\n if (other instanceof FirestoreStateDocumentDoubleField) {\n return new FirestoreStateDocumentDoubleField(this.value + other.value);\n }\n throw new Error(`unsupported type. other=${other}`);\n }\n}\n\nexport class FirestoreStateDocumentTimestampField\n implements FirestoreStateDocumentBaseField\n{\n type = \"timestamp_value\" as const;\n constructor(readonly value: { nanos: number; seconds: number }) {}\n\n static fromDate(date: Date) {\n return new FirestoreStateDocumentTimestampField({\n nanos: (date.getTime() % 1000) * 1000000,\n seconds: Math.floor(date.getTime() / 1000),\n });\n }\n\n toJSON() {\n return { type: this.type, value: this.value } as const;\n }\n\n toV1ValueObject(): ValueObjectType {\n return { timestamp_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentTimestampField &&\n this.value.seconds === other.value.seconds &&\n this.value.nanos === other.value.nanos\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentTimestampField &&\n (this.value.seconds < other.value.seconds ||\n (this.value.seconds === other.value.seconds &&\n this.value.nanos < other.value.nanos))\n );\n }\n\n lte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentTimestampField &&\n (this.value.seconds < other.value.seconds ||\n (this.value.seconds === other.value.seconds &&\n this.value.nanos <= other.value.nanos))\n );\n }\n\n gt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentTimestampField &&\n (this.value.seconds > other.value.seconds ||\n (this.value.seconds === other.value.seconds &&\n this.value.nanos > other.value.nanos))\n );\n }\n\n gte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentTimestampField &&\n (this.value.seconds > other.value.seconds ||\n (this.value.seconds === other.value.seconds &&\n this.value.nanos >= other.value.nanos))\n );\n }\n}\n\nexport class FirestoreStateDocumentBytesField\n implements FirestoreStateDocumentBaseField\n{\n type = \"bytes_value\" as const;\n constructor(readonly value: Uint8Array) {}\n\n toJSON() {\n return { type: this.type, value: this.value } as const;\n }\n\n toV1ValueObject(): ValueObjectType {\n return { bytes_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBytesField &&\n this.value.toString() === other.value.toString()\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBytesField &&\n this.value.toString() < other.value.toString()\n );\n }\n\n lte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBytesField &&\n this.value.toString() <= other.value.toString()\n );\n }\n\n gt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBytesField &&\n this.value.toString() > other.value.toString()\n );\n }\n\n gte(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentBytesField &&\n this.value.toString() >= other.value.toString()\n );\n }\n}\n\nexport class FirestoreStateDocumentReferenceField\n implements FirestoreStateDocumentBaseField\n{\n type = \"reference_value\" as const;\n constructor(readonly value: string) {}\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { reference_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentReferenceField &&\n this.value === other.value\n );\n }\n\n lt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n lte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gt(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n\n gte(_other: FirestoreStateDocumentFields): boolean {\n return false;\n }\n}\n\nexport class FirestoreStateDocumentGeoPointField\n implements FirestoreStateDocumentBaseField\n{\n type = \"geo_point_value\" as const;\n constructor(readonly value: { latitude: number; longitude: number }) {}\n\n toJSON() {\n return { type: this.type, value: this.value };\n }\n\n toV1ValueObject(): ValueObjectType {\n return { geo_point_value: this.value };\n }\n\n eq(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentGeoPointField &&\n this.value.latitude === other.value.latitude &&\n this.value.longitude === other.value.longitude\n );\n }\n\n lt(other: FirestoreStateDocumentFields): boolean {\n return (\n other instanceof FirestoreStateDocumentGeoPointF