UNPKG

firebase-tools

Version:
650 lines (649 loc) 27.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FirestoreApi = void 0; const clc = require("colorette"); const logger_1 = require("../logger"); const utils = require("../utils"); const validator = require("./validator"); const types = require("./api-types"); const api_types_1 = require("./api-types"); const sort = require("./api-sort"); const util = require("./util"); const prompt_1 = require("../prompt"); const api_1 = require("../api"); const error_1 = require("../error"); const apiv2_1 = require("../apiv2"); const pretty_print_1 = require("./pretty-print"); const functional_1 = require("../functional"); const operation_poller_1 = require("../operation-poller"); class FirestoreApi { constructor() { this.apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.firestoreOrigin)(), apiVersion: "v1" }); this.printer = new pretty_print_1.PrettyPrint(); } static processIndex(index) { var _a; let fields = index.fields; const suffixOrder = FirestoreApi.lastIndexFieldOrder(fields); const nameSuffix = { fieldPath: "__name__", order: suffixOrder }; const lastField = (_a = index.fields) === null || _a === void 0 ? void 0 : _a[index.fields.length - 1]; if (lastField.vectorConfig) { const vectorField = lastField; fields = fields.slice(0, -1); if (fields.length === 0 || (fields === null || fields === void 0 ? void 0 : fields[fields.length - 1].fieldPath) !== "__name__") { fields.push(nameSuffix); } fields.push(vectorField); return Object.assign(Object.assign({}, index), { fields }); } if ((lastField === null || lastField === void 0 ? void 0 : lastField.fieldPath) !== "__name__") { fields.push(nameSuffix); } return Object.assign(Object.assign({}, index), { fields }); } static lastIndexFieldOrder(fields) { let lastIndexFieldOrder = types.Order.ASCENDING; for (const field of fields) { if (field.order) { lastIndexFieldOrder = field.order; } } return lastIndexFieldOrder; } async deploy(options, indexes, fieldOverrides, databaseId = "(default)") { var _a; const spec = this.upgradeOldSpec({ indexes, fieldOverrides, }); this.validateSpec(spec); const indexesToDeploy = spec.indexes; const fieldOverridesToDeploy = spec.fieldOverrides; const existingIndexes = await this.listIndexes(options.project, databaseId); const existingFieldOverrides = await this.listFieldOverrides(options.project, databaseId); const database = await this.getDatabase(options.project, databaseId); const edition = (_a = database.databaseEdition) !== null && _a !== void 0 ? _a : api_types_1.DatabaseEdition.STANDARD; const indexesToDelete = existingIndexes.filter((index) => { return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec, edition)); }); const fieldOverridesToDelete = existingFieldOverrides.filter((field) => { return !fieldOverridesToDeploy.some((spec) => { const parsedName = util.parseFieldName(field.name); if (parsedName.collectionGroupId !== spec.collectionGroup) { return false; } if (parsedName.fieldPath !== spec.fieldPath) { return false; } return true; }); }); let shouldDeleteIndexes = options.force; if (indexesToDelete.length > 0) { if (options.nonInteractive && !options.force) { utils.logLabeledBullet("firestore", `there are ${indexesToDelete.length} indexes defined in your project that are not present in your ` + "firestore indexes file. To delete them, run this command with the --force flag."); } else if (!options.force) { const indexesString = indexesToDelete .map((x) => this.printer.prettyIndexString(x, false)) .join("\n\t"); utils.logLabeledBullet("firestore", `The following indexes are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`); } if (!shouldDeleteIndexes) { shouldDeleteIndexes = await (0, prompt_1.confirm)({ nonInteractive: options.nonInteractive, force: options.force, default: false, message: "Would you like to delete these indexes? Selecting no will continue the rest of the deployment.", }); } } for (const index of indexesToDeploy) { const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index, edition)); if (exists) { logger_1.logger.debug(`Skipping existing index: ${JSON.stringify(index)}`); } else { logger_1.logger.debug(`Creating new index: ${JSON.stringify(index)}`); await this.createIndex(options.project, index, databaseId); } } if (shouldDeleteIndexes && indexesToDelete.length > 0) { utils.logLabeledBullet("firestore", `Deleting ${indexesToDelete.length} indexes...`); for (const index of indexesToDelete) { await this.deleteIndex(index); } } let shouldDeleteFields = options.force; if (fieldOverridesToDelete.length > 0) { if (options.nonInteractive && !options.force) { utils.logLabeledBullet("firestore", `there are ${fieldOverridesToDelete.length} field overrides defined in your project that are not present in your ` + "firestore indexes file. To delete them, run this command with the --force flag."); } else if (!options.force) { const indexesString = fieldOverridesToDelete .map((x) => this.printer.prettyFieldString(x)) .join("\n\t"); utils.logLabeledBullet("firestore", `The following field overrides are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`); } if (!shouldDeleteFields) { shouldDeleteFields = await (0, prompt_1.confirm)({ nonInteractive: options.nonInteractive, force: options.force, default: false, message: "Would you like to delete these field overrides? Selecting no will continue the rest of the deployment.", }); } } const sortedFieldOverridesToDeploy = fieldOverridesToDeploy.sort(sort.compareFieldOverride); for (const field of sortedFieldOverridesToDeploy) { const exists = existingFieldOverrides.some((x) => this.fieldMatchesSpec(x, field)); if (exists) { logger_1.logger.debug(`Skipping existing field override: ${JSON.stringify(field)}`); } else { logger_1.logger.debug(`Updating field override: ${JSON.stringify(field)}`); await this.patchField(options.project, field, databaseId); } } if (shouldDeleteFields && fieldOverridesToDelete.length > 0) { utils.logLabeledBullet("firestore", `Deleting ${fieldOverridesToDelete.length} field overrides...`); for (const field of fieldOverridesToDelete) { await this.deleteField(field); } } } async listIndexes(project, databaseId = "(default)") { const url = `/projects/${project}/databases/${databaseId}/collectionGroups/-/indexes`; const res = await this.apiClient.get(url); const indexes = res.body.indexes; if (!indexes) { return []; } return indexes; } async listFieldOverrides(project, databaseId = "(default)") { const parent = `projects/${project}/databases/${databaseId}/collectionGroups/-`; const url = `/${parent}/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:*`; const res = await this.apiClient.get(url); const fields = res.body.fields; if (!fields) { return []; } return fields.filter((field) => { return !field.name.includes("__default__"); }); } makeIndexSpec(indexes, fields) { const indexesJson = indexes.map((index) => { return { collectionGroup: util.parseIndexName(index.name).collectionGroupId, queryScope: index.queryScope, fields: index.fields, apiScope: index.apiScope, density: index.density, multikey: index.multikey, unique: index.unique, }; }); if (!fields) { logger_1.logger.debug("No field overrides specified, using []."); fields = []; } const fieldsJson = fields.map((field) => { const parsedName = util.parseFieldName(field.name); const fieldIndexes = field.indexConfig.indexes || []; return { collectionGroup: parsedName.collectionGroupId, fieldPath: parsedName.fieldPath, ttl: !!field.ttlConfig, indexes: fieldIndexes.map((index) => { const firstField = index.fields[0]; return { order: firstField.order, arrayConfig: firstField.arrayConfig, queryScope: index.queryScope, apiScope: index.apiScope, density: index.density, multikey: index.multikey, unique: index.unique, }; }), }; }); const sortedIndexes = indexesJson.sort(sort.compareSpecIndex); const sortedFields = fieldsJson.sort(sort.compareFieldOverride); return { indexes: sortedIndexes, fieldOverrides: sortedFields, }; } validateSpec(spec) { validator.assertHas(spec, "indexes"); spec.indexes.forEach((index) => { this.validateIndex(index); }); if (spec.fieldOverrides) { spec.fieldOverrides.forEach((field) => { this.validateField(field); }); } } validateIndex(index) { validator.assertHas(index, "collectionGroup"); validator.assertHas(index, "queryScope"); validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); if (index.apiScope) { validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); } if (index.density) { validator.assertEnum(index, "density", Object.keys(types.Density)); } if (index.multikey) { validator.assertType("multikey", index.multikey, "boolean"); } if (index.unique !== undefined) { validator.assertType("unique", index.unique, "boolean"); throw new error_1.FirebaseError("The `unique` index configuration is not supported yet."); } validator.assertHas(index, "fields"); index.fields.forEach((field) => { validator.assertHas(field, "fieldPath"); validator.assertHasOneOf(field, ["order", "arrayConfig", "vectorConfig"]); if (field.order) { validator.assertEnum(field, "order", Object.keys(types.Order)); } if (field.arrayConfig) { validator.assertEnum(field, "arrayConfig", Object.keys(types.ArrayConfig)); } if (field.vectorConfig) { validator.assertType("vectorConfig.dimension", field.vectorConfig.dimension, "number"); validator.assertHas(field.vectorConfig, "flat"); } }); } validateField(field) { validator.assertHas(field, "collectionGroup"); validator.assertHas(field, "fieldPath"); validator.assertHas(field, "indexes"); if (typeof field.ttl !== "undefined") { validator.assertType("ttl", field.ttl, "boolean"); } field.indexes.forEach((index) => { validator.assertHasOneOf(index, ["arrayConfig", "order"]); if (index.arrayConfig) { validator.assertEnum(index, "arrayConfig", Object.keys(types.ArrayConfig)); } if (index.order) { validator.assertEnum(index, "order", Object.keys(types.Order)); } if (index.queryScope) { validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); } if (index.apiScope) { validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); } if (index.density) { validator.assertEnum(index, "density", Object.keys(types.Density)); } if (index.multikey) { validator.assertType("multikey", index.multikey, "boolean"); } if (index.unique) { validator.assertType("unique", index.unique, "boolean"); } }); } async patchField(project, spec, databaseId = "(default)") { const url = `/projects/${project}/databases/${databaseId}/collectionGroups/${spec.collectionGroup}/fields/${spec.fieldPath}`; const indexes = spec.indexes.map((index) => { return { queryScope: index.queryScope, apiScope: index.apiScope, density: index.density, multikey: index.multikey, unique: index.unique, fields: [ { fieldPath: spec.fieldPath, arrayConfig: index.arrayConfig, order: index.order, }, ], }; }); let data = { indexConfig: { indexes, }, }; if (spec.ttl) { data = Object.assign(data, { ttlConfig: {}, }); } if (typeof spec.ttl !== "undefined") { await this.apiClient.patch(url, data); } else { await this.apiClient.patch(url, data, { queryParams: { updateMask: "indexConfig" } }); } } deleteField(field) { const url = field.name; const data = {}; return this.apiClient.patch(`/${url}`, data); } createIndex(project, index, databaseId = "(default)") { const url = `/projects/${project}/databases/${databaseId}/collectionGroups/${index.collectionGroup}/indexes`; return this.apiClient.post(url, { fields: index.fields, queryScope: index.queryScope, apiScope: index.apiScope, density: index.density, multikey: index.multikey, unique: index.unique, }); } deleteIndex(index) { const url = index.name; return this.apiClient.delete(`/${url}`); } optionalApiScopeMatches(lhs, rhs) { return (0, functional_1.optionalValueMatches)(lhs, rhs, types.ApiScope.ANY_API); } optionalDensityMatches(lhs, rhs, edition) { const defaultValue = edition === api_types_1.DatabaseEdition.STANDARD ? types.Density.SPARSE_ALL : types.Density.DENSE; return (0, functional_1.optionalValueMatches)(lhs, rhs, defaultValue); } optionalMultikeyMatches(lhs, rhs) { const defaultValue = false; return (0, functional_1.optionalValueMatches)(lhs, rhs, defaultValue); } indexMatchesSpec(index, spec, edition) { const collection = util.parseIndexName(index.name).collectionGroupId; if (collection !== spec.collectionGroup) { return false; } if (index.queryScope !== spec.queryScope) { return false; } if (!this.optionalApiScopeMatches(index.apiScope, spec.apiScope)) { return false; } if (!this.optionalDensityMatches(index.density, spec.density, edition)) { return false; } if (!this.optionalMultikeyMatches(index.multikey, spec.multikey)) { return false; } let specIdx = spec; if (edition === api_types_1.DatabaseEdition.STANDARD) { specIdx = FirestoreApi.processIndex(specIdx); } if (index.fields.length !== specIdx.fields.length) { return false; } let i = 0; while (i < index.fields.length) { const iField = index.fields[i]; const sField = specIdx.fields[i]; if (iField.fieldPath !== sField.fieldPath) { return false; } if (iField.order !== sField.order) { return false; } if (iField.arrayConfig !== sField.arrayConfig) { return false; } if (!utils.deepEqual(iField.vectorConfig, sField.vectorConfig)) { return false; } i++; } return true; } fieldMatchesSpec(field, spec) { const parsedName = util.parseFieldName(field.name); if (parsedName.collectionGroupId !== spec.collectionGroup) { return false; } if (parsedName.fieldPath !== spec.fieldPath) { return false; } if (typeof spec.ttl !== "undefined" && util.booleanXOR(!!field.ttlConfig, spec.ttl)) { return false; } else if (!!field.ttlConfig && typeof spec.ttl === "undefined") { utils.logLabeledBullet("firestore", `there are TTL field overrides for collection ${spec.collectionGroup} defined in your project that are not present in your ` + "firestore indexes file. The TTL policy won't be deleted since is not specified as false."); } const fieldIndexes = field.indexConfig.indexes || []; if (fieldIndexes.length !== spec.indexes.length) { return false; } const fieldModes = fieldIndexes.map((index) => { const firstField = index.fields[0]; return firstField.order || firstField.arrayConfig; }); const specModes = spec.indexes.map((index) => { return index.order || index.arrayConfig; }); for (const mode of fieldModes) { if (!specModes.includes(mode)) { return false; } } return true; } upgradeOldSpec(spec) { const result = { indexes: [], fieldOverrides: spec.fieldOverrides || [], }; if (!(spec.indexes && spec.indexes.length > 0)) { return result; } if (spec.indexes[0].collectionId) { utils.logBullet(clc.bold(clc.cyan("firestore:")) + " your indexes indexes are specified in the v1beta1 API format. " + "Please upgrade to the new index API format by running " + clc.bold("firebase firestore:indexes") + " again and saving the result."); } result.indexes = spec.indexes.map((index) => { const i = { collectionGroup: index.collectionGroup || index.collectionId, queryScope: index.queryScope || types.QueryScope.COLLECTION, }; if (index.apiScope) { i.apiScope = index.apiScope; } if (index.density) { i.density = index.density; } if (index.multikey !== undefined) { i.multikey = index.multikey; } if (index.unique !== undefined) { i.unique = index.unique; } if (index.fields) { i.fields = index.fields.map((field) => { const f = { fieldPath: field.fieldPath, }; if (field.order) { f.order = field.order; } else if (field.arrayConfig) { f.arrayConfig = field.arrayConfig; } else if (field.vectorConfig) { f.vectorConfig = field.vectorConfig; } else if (field.mode === types.Mode.ARRAY_CONTAINS) { f.arrayConfig = types.ArrayConfig.CONTAINS; } else { f.order = field.mode; } return f; }); } return i; }); return result; } async listDatabases(project) { const url = `/projects/${project}/databases`; const res = await this.apiClient.get(url); const databases = res.body.databases; if (!databases) { return []; } return databases; } async locations(project) { const url = `/projects/${project}/locations`; const res = await this.apiClient.get(url); const locations = res.body.locations; if (!locations) { return []; } return locations; } async getDatabase(project, databaseId) { const url = `/projects/${project}/databases/${databaseId}`; const res = await this.apiClient.get(url); const database = res.body; if (!database) { throw new error_1.FirebaseError("Not found"); } return database; } async createDatabase(req) { const url = `/projects/${req.project}/databases`; const payload = { locationId: req.locationId, type: req.type, databaseEdition: req.databaseEdition, deleteProtectionState: req.deleteProtectionState, pointInTimeRecoveryEnablement: req.pointInTimeRecoveryEnablement, cmekConfig: req.cmekConfig, }; const options = { queryParams: { databaseId: req.databaseId } }; const res = await this.apiClient.post(url, payload, options); await (0, operation_poller_1.pollOperation)({ apiOrigin: (0, api_1.firestoreOrigin)(), apiVersion: "v1", operationResourceName: res.body.name, masterTimeout: 600000, }); const database = res.body.response; if (!database) { throw new error_1.FirebaseError("Not found"); } return database; } async updateDatabase(project, databaseId, deleteProtectionState, pointInTimeRecoveryEnablement) { const url = `/projects/${project}/databases/${databaseId}`; const payload = { deleteProtectionState, pointInTimeRecoveryEnablement, }; const res = await this.apiClient.patch(url, payload); const database = res.body.response; if (!database) { throw new error_1.FirebaseError("Not found"); } return database; } async deleteDatabase(project, databaseId) { const url = `/projects/${project}/databases/${databaseId}`; const res = await this.apiClient.delete(url); const database = res.body.response; if (!database) { throw new error_1.FirebaseError("Not found"); } return database; } async bulkDeleteDocuments(project, databaseId, collectionIds) { var _a; const name = `/projects/${project}/databases/${databaseId}`; const url = `${name}:bulkDeleteDocuments`; const payload = { name, collectionIds, }; const res = await this.apiClient.post(url, payload); return { name: (_a = res.body) === null || _a === void 0 ? void 0 : _a.name, }; } async restoreDatabase(project, databaseId, backupName, encryptionConfig) { const url = `/projects/${project}/databases:restore`; const payload = { databaseId, backup: backupName, encryptionConfig: encryptionConfig, }; const options = { queryParams: { databaseId: databaseId } }; const res = await this.apiClient.post(url, payload, options); const database = res.body.response; if (!database) { throw new error_1.FirebaseError("Not found"); } return database; } async cloneDatabase(project, pitrSnapshot, databaseId, encryptionConfig) { const url = `/projects/${project}/databases:clone`; const payload = { databaseId, pitrSnapshot, encryptionConfig, }; const options = { queryParams: { databaseId: databaseId } }; const res = await this.apiClient.post(url, payload, options); const lro = res.body; if (!lro) { throw new error_1.FirebaseError("Not found"); } return lro; } async listOperations(project, databaseId, limit) { const url = `/projects/${project}/databases/${databaseId}/operations`; const res = await this.apiClient.get(url, { queryParams: { pageSize: limit, }, }); return res.body; } async describeOperation(project, databaseId, operationName) { const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}`; const res = await this.apiClient.get(url); return res.body; } async cancelOperation(project, databaseId, operationName) { var _a, _b, _c, _d; const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}:cancel`; try { const res = await this.apiClient.post(url); return { success: res.status === 200 }; } catch (error) { const reason = "Cannot cancel an operation that is completed."; const details = ((_c = (_b = (_a = error.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.details) || []; for (const detail of details) { if ((_d = detail.detail) === null || _d === void 0 ? void 0 : _d.includes(reason)) { throw new error_1.FirebaseError(reason); } } throw error; } } } exports.FirestoreApi = FirestoreApi;