@firestore-emulator/server
Version:
This package is the implementation of the Firestore emulator. It is a Node.js
1,471 lines (1,462 loc) • 55.4 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
DateFromTimestamp: () => DateFromTimestamp,
FirestoreServer: () => FirestoreServer,
FirestoreState: () => FirestoreState,
FirestoreStateCollection: () => FirestoreStateCollection,
FirestoreStateDatabase: () => FirestoreStateDatabase,
FirestoreStateDocument: () => FirestoreStateDocument,
FirestoreStateProject: () => FirestoreStateProject,
TimestampFromDate: () => TimestampFromDate,
TimestampFromNow: () => TimestampFromNow
});
module.exports = __toCommonJS(index_exports);
// src/FirestoreState/index.ts
var import_node_events = require("events");
var import_document = require("@firestore-emulator/proto/dist/google/firestore/v1/document");
var import_firestore = require("@firestore-emulator/proto/dist/google/firestore/v1/firestore");
var import_query = require("@firestore-emulator/proto/dist/google/firestore/v1/query");
var import_write = require("@firestore-emulator/proto/dist/google/firestore/v1/write");
var import_timestamp = require("@firestore-emulator/proto/dist/google/protobuf/timestamp");
var import_constants = require("@grpc/grpc-js/build/src/constants");
var import_assert_never = require("assert-never");
var import_immer2 = require("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
var import_struct = require("@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: import_struct.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
var import_immer = require("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 (0, import_immer.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 = (0, import_immer2.produce)(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 import_document.Document.fromObject({
create_time: this.metadata.hasExist ? import_timestamp.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 ? import_timestamp.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 = (0, import_immer2.produce)(this.metadata, (draft) => {
draft.hasExist = true;
draft.createdAt = date;
draft.updatedAt = date;
});
this.fields = (0, import_immer2.produce)(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 = (0, import_immer2.produce)(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 = (0, import_immer2.produce)(this.metadata, (draft) => {
draft.hasExist = true;
draft.createdAt = date;
draft.updatedAt = date;
});
} else {
this.metadata = (0, import_immer2.produce)(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 = (0, import_immer2.produce)(this.metadata, (draft) => {
draft.hasExist = false;
draft.createdAt = null;
draft.updatedAt = null;
});
this.fields = (0, import_immer2.produce)(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 === import_write.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 = (0, import_immer2.produce)(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 = (0, import_immer2.produce)(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 = (0, import_immer2.produce)(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) => import_timestamp.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 import_query.StructuredQueryFieldFilterOperator.EQUAL:
return field.eq(filterValue);
case import_query.StructuredQueryFieldFilterOperator.LESS_THAN:
return field.lt(filterValue);
case import_query.StructuredQueryFieldFilterOperator.LESS_THAN_OR_EQUAL:
return field.lte(filterValue);
case import_query.StructuredQueryFieldFilterOperator.GREATER_THAN:
return field.gt(filterValue);
case import_query.StructuredQueryFieldFilterOperator.GREATER_THAN_OR_EQUAL:
return field.gte(filterValue);
case import_query.StructuredQueryFieldFilterOperator.ARRAY_CONTAINS:
return field instanceof FirestoreStateDocumentArrayField && field.value.some((value) => value.eq(filterValue));
case import_query.StructuredQueryFieldFilterOperator.ARRAY_CONTAINS_ANY:
return field instanceof FirestoreStateDocumentArrayField && filterValue instanceof FirestoreStateDocumentArrayField && field.value.some(
(value) => filterValue.value.some((filterValue2) => value.eq(filterValue2))
);
case import_query.StructuredQueryFieldFilterOperator.IN:
return filterValue instanceof FirestoreStateDocumentArrayField && filterValue.value.some((value) => field.eq(value));
case import_query.StructuredQueryFieldFilterOperator.NOT_EQUAL:
return !field.eq(filterValue);
case import_query.StructuredQueryFieldFilterOperator.NOT_IN:
return filterValue instanceof FirestoreStateDocumentArrayField && filterValue.value.every((value) => !field.eq(value));
case import_query.StructuredQueryFieldFilterOperator.OPERATOR_UNSPECIFIED: {
throw new Error(
`Invalid query: op is not supported yet, ${import_query.StructuredQueryFieldFilterOperator[filter.op]}`
);
}
case void 0:
throw new Error("Invalid query: op is required");
default:
(0, import_assert_never.assertNever)(filter.op);
}
};
var FirestoreState = class {
constructor(projects = {}) {
this.projects = projects;
this.emitter = new import_node_events.EventEmitter();
}
emitter;
toJSON() {
return {
projects: Object.fromEntries(
Object.entries(this.projects).map(([key, project]) => [
key,
project.toJSON()
])
)
};
}
getProject(projectName) {
this.projects = (0, import_immer2.produce)(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(
import_constants.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(
import_constants.Status.ALREADY_EXISTS,
`entity already exists: EntityRef{partitionRef=dev~${document.database.project.name}, path=/${document.getDocumentPath()}}`
);
}
}
}
if (document.metadata.hasExist) {
document.delete();
}
return import_write.WriteResult.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(
import_constants.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(
import_constants.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 import_write.WriteResult.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 === import_query.StructuredQueryDirection.ASCENDING) {
if (a.lt(b)) return -1;
if (b.lt(a)) return 1;
} else if (direction === import_query.StructuredQueryDirection.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 import_query.StructuredQueryCompositeFilterOperator.AND:
case import_query.StructuredQueryCompositeFilterOperator.OR:
break;
case import_query.StructuredQueryCompositeFilterOperator.OPERATOR_UNSPECIFIED:
throw new Error("Invalid query: op is required");
default:
(0, import_assert_never.assertNever)(query.where.composite_filter.op);
}
docs = docs.filter((document) => {
if (query.where.composite_filter.op === import_query.StructuredQueryCompositeFilterOperator.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 === import_query.StructuredQueryCompositeFilterOperator.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(
import_firestore.ListenResponse.fromObject({
document_change: {
document: doc.toV1DocumentObject(),
target_ids: [listen.add_target.target_id]
}
})
);
}
callback(
import_firestore.ListenResponse.fromObject({
target_change: {
read_time: TimestampFromNow().toObject(),
target_change_type: import_firestore.TargetChangeTargetChangeType.CURRENT,
target_ids: [listen.add_target.target_id]
}
})
);
callback(
import_firestore.ListenResponse.fromObject({
target_change: {
read_time: TimestampFromNow().toObject(),
target_change_type: import_firestore.TargetChangeTargetChangeType.NO_CHANGE,
target_ids: []
}
})
);
};
if (listen.add_target.has_query) {
let currentDocumentsPaths = [];
callback(
import_firestore.ListenResponse.fromObject({
target_change: {
target_change_type: import_firestore.TargetChangeTargetChangeType.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(
import_firestore.ListenResponse.fromObject({
target_change: {
read_time: TimestampFromNow().toObject(),
target_change_type: import_firestore.TargetChangeTargetChangeType.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(
import_firestore.ListenResponse.fromObject({
target_change: {
target_change_type: import_firestore.TargetChangeTargetChangeType.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(
import_firestore.ListenResponse.fromObject({
target_change: {
read_time: TimestampFromNow().toObject(),
target_change_type: import_firestore.TargetChangeTargetChangeType.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
var import_grpc_js = require("@grpc/grpc-js");
// src/server/FirestoreServiceV1Impl/index.ts
var import_firestore2 = require("@firestore-emulator/proto/dist/google/firestore/v1/firestore");
var import_empty = require("@firestore-emulator/proto/dist/google/protobuf/empty");
var FirestoreServiceV1Impl = class extends import_firestore2.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(
import_firestore2.BatchGetDocumentsResponse.fromObject({
transaction: tx
})
);
}
for (const documentPath of call.request.documents) {
const document = this.#state.getDocument(documentPath);
call.write(
import_firestore2.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,
import_firestore2.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,
import_firestore2.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, import_empty.Empty.fromObject({}));
}
RunQuery(call) {
const date = /* @__PURE__ */ new Date();
const results = this.#state.v1Query(
call.request.parent,
ca