123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.FirestoreIndexes = void 0;
- const clc = require("colorette");
- const logger_1 = require("../logger");
- const utils = require("../utils");
- const validator = require("./validator");
- const API = require("./indexes-api");
- const sort = require("./indexes-sort");
- const util = require("./util");
- const prompt_1 = require("../prompt");
- const api_1 = require("../api");
- const apiv2_1 = require("../apiv2");
- class FirestoreIndexes {
- constructor() {
- this.apiClient = new apiv2_1.Client({ urlPrefix: api_1.firestoreOrigin, apiVersion: "v1" });
- }
- async deploy(options, indexes, fieldOverrides) {
- const spec = this.upgradeOldSpec({
- indexes,
- fieldOverrides,
- });
- this.validateSpec(spec);
- const indexesToDeploy = spec.indexes;
- const fieldOverridesToDeploy = spec.fieldOverrides;
- const existingIndexes = await this.listIndexes(options.project);
- const existingFieldOverrides = await this.listFieldOverrides(options.project);
- const indexesToDelete = existingIndexes.filter((index) => {
- return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec));
- });
- 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.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.promptOnce)({
- type: "confirm",
- name: "confirm",
- 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));
- 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);
- }
- }
- 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.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.promptOnce)({
- type: "confirm",
- name: "confirm",
- 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);
- }
- }
- 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) {
- const url = `/projects/${project}/databases/(default)/collectionGroups/-/indexes`;
- const res = await this.apiClient.get(url);
- const indexes = res.body.indexes;
- if (!indexes) {
- return [];
- }
- return indexes.map((index) => {
- const fields = index.fields.filter((field) => {
- return field.fieldPath !== "__name__";
- });
- return {
- name: index.name,
- state: index.state,
- queryScope: index.queryScope,
- fields,
- };
- });
- }
- async listFieldOverrides(project) {
- const parent = `projects/${project}/databases/(default)/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.indexOf("__default__") < 0;
- });
- }
- makeIndexSpec(indexes, fields) {
- const indexesJson = indexes.map((index) => {
- return {
- collectionGroup: util.parseIndexName(index.name).collectionGroupId,
- queryScope: index.queryScope,
- fields: index.fields,
- };
- });
- 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,
- };
- }),
- };
- });
- const sortedIndexes = indexesJson.sort(sort.compareSpecIndex);
- const sortedFields = fieldsJson.sort(sort.compareFieldOverride);
- return {
- indexes: sortedIndexes,
- fieldOverrides: sortedFields,
- };
- }
- prettyPrintIndexes(indexes) {
- if (indexes.length === 0) {
- logger_1.logger.info("None");
- return;
- }
- const sortedIndexes = indexes.sort(sort.compareApiIndex);
- sortedIndexes.forEach((index) => {
- logger_1.logger.info(this.prettyIndexString(index));
- });
- }
- printFieldOverrides(fields) {
- if (fields.length === 0) {
- logger_1.logger.info("None");
- return;
- }
- const sortedFields = fields.sort(sort.compareApiField);
- sortedFields.forEach((field) => {
- logger_1.logger.info(this.prettyFieldString(field));
- });
- }
- 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(API.QueryScope));
- validator.assertHas(index, "fields");
- index.fields.forEach((field) => {
- validator.assertHas(field, "fieldPath");
- validator.assertHasOneOf(field, ["order", "arrayConfig"]);
- if (field.order) {
- validator.assertEnum(field, "order", Object.keys(API.Order));
- }
- if (field.arrayConfig) {
- validator.assertEnum(field, "arrayConfig", Object.keys(API.ArrayConfig));
- }
- });
- }
- 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(API.ArrayConfig));
- }
- if (index.order) {
- validator.assertEnum(index, "order", Object.keys(API.Order));
- }
- if (index.queryScope) {
- validator.assertEnum(index, "queryScope", Object.keys(API.QueryScope));
- }
- });
- }
- async patchField(project, spec) {
- const url = `/projects/${project}/databases/(default)/collectionGroups/${spec.collectionGroup}/fields/${spec.fieldPath}`;
- const indexes = spec.indexes.map((index) => {
- return {
- queryScope: index.queryScope,
- 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) {
- const url = `/projects/${project}/databases/(default)/collectionGroups/${index.collectionGroup}/indexes`;
- return this.apiClient.post(url, {
- fields: index.fields,
- queryScope: index.queryScope,
- });
- }
- deleteIndex(index) {
- const url = index.name;
- return this.apiClient.delete(`/${url}`);
- }
- indexMatchesSpec(index, spec) {
- const collection = util.parseIndexName(index.name).collectionGroupId;
- if (collection !== spec.collectionGroup) {
- return false;
- }
- if (index.queryScope !== spec.queryScope) {
- return false;
- }
- if (index.fields.length !== spec.fields.length) {
- return false;
- }
- let i = 0;
- while (i < index.fields.length) {
- const iField = index.fields[i];
- const sField = spec.fields[i];
- if (iField.fieldPath !== sField.fieldPath) {
- return false;
- }
- if (iField.order !== sField.order) {
- return false;
- }
- if (iField.arrayConfig !== sField.arrayConfig) {
- 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.indexOf(mode) < 0) {
- 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 || API.QueryScope.COLLECTION,
- fields: [],
- };
- 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.mode === API.Mode.ARRAY_CONTAINS) {
- f.arrayConfig = API.ArrayConfig.CONTAINS;
- }
- else {
- f.order = field.mode;
- }
- return f;
- });
- }
- return i;
- });
- return result;
- }
- prettyIndexString(index, includeState = true) {
- let result = "";
- if (index.state && includeState) {
- const stateMsg = `[${index.state}] `;
- if (index.state === API.State.READY) {
- result += clc.green(stateMsg);
- }
- else if (index.state === API.State.CREATING) {
- result += clc.yellow(stateMsg);
- }
- else {
- result += clc.red(stateMsg);
- }
- }
- const nameInfo = util.parseIndexName(index.name);
- result += clc.cyan(`(${nameInfo.collectionGroupId})`);
- result += " -- ";
- index.fields.forEach((field) => {
- if (field.fieldPath === "__name__") {
- return;
- }
- const orderOrArrayConfig = field.order ? field.order : field.arrayConfig;
- result += `(${field.fieldPath},${orderOrArrayConfig}) `;
- });
- return result;
- }
- prettyFieldString(field) {
- let result = "";
- const parsedName = util.parseFieldName(field.name);
- result +=
- "[" +
- clc.cyan(parsedName.collectionGroupId) +
- "." +
- clc.yellow(parsedName.fieldPath) +
- "] --";
- const fieldIndexes = field.indexConfig.indexes || [];
- if (fieldIndexes.length > 0) {
- fieldIndexes.forEach((index) => {
- const firstField = index.fields[0];
- const mode = firstField.order || firstField.arrayConfig;
- result += ` (${mode})`;
- });
- }
- else {
- result += " (no indexes)";
- }
- const fieldTtl = field.ttlConfig;
- if (fieldTtl) {
- result += ` TTL(${fieldTtl.state})`;
- }
- return result;
- }
- }
- exports.FirestoreIndexes = FirestoreIndexes;
|