No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

indexes.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FirestoreIndexes = void 0;
  4. const clc = require("colorette");
  5. const logger_1 = require("../logger");
  6. const utils = require("../utils");
  7. const validator = require("./validator");
  8. const API = require("./indexes-api");
  9. const sort = require("./indexes-sort");
  10. const util = require("./util");
  11. const prompt_1 = require("../prompt");
  12. const api_1 = require("../api");
  13. const apiv2_1 = require("../apiv2");
  14. class FirestoreIndexes {
  15. constructor() {
  16. this.apiClient = new apiv2_1.Client({ urlPrefix: api_1.firestoreOrigin, apiVersion: "v1" });
  17. }
  18. async deploy(options, indexes, fieldOverrides) {
  19. const spec = this.upgradeOldSpec({
  20. indexes,
  21. fieldOverrides,
  22. });
  23. this.validateSpec(spec);
  24. const indexesToDeploy = spec.indexes;
  25. const fieldOverridesToDeploy = spec.fieldOverrides;
  26. const existingIndexes = await this.listIndexes(options.project);
  27. const existingFieldOverrides = await this.listFieldOverrides(options.project);
  28. const indexesToDelete = existingIndexes.filter((index) => {
  29. return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec));
  30. });
  31. const fieldOverridesToDelete = existingFieldOverrides.filter((field) => {
  32. return !fieldOverridesToDeploy.some((spec) => {
  33. const parsedName = util.parseFieldName(field.name);
  34. if (parsedName.collectionGroupId !== spec.collectionGroup) {
  35. return false;
  36. }
  37. if (parsedName.fieldPath !== spec.fieldPath) {
  38. return false;
  39. }
  40. return true;
  41. });
  42. });
  43. let shouldDeleteIndexes = options.force;
  44. if (indexesToDelete.length > 0) {
  45. if (options.nonInteractive && !options.force) {
  46. utils.logLabeledBullet("firestore", `there are ${indexesToDelete.length} indexes defined in your project that are not present in your ` +
  47. "firestore indexes file. To delete them, run this command with the --force flag.");
  48. }
  49. else if (!options.force) {
  50. const indexesString = indexesToDelete
  51. .map((x) => this.prettyIndexString(x, false))
  52. .join("\n\t");
  53. utils.logLabeledBullet("firestore", `The following indexes are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`);
  54. }
  55. if (!shouldDeleteIndexes) {
  56. shouldDeleteIndexes = await (0, prompt_1.promptOnce)({
  57. type: "confirm",
  58. name: "confirm",
  59. default: false,
  60. message: "Would you like to delete these indexes? Selecting no will continue the rest of the deployment.",
  61. });
  62. }
  63. }
  64. for (const index of indexesToDeploy) {
  65. const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index));
  66. if (exists) {
  67. logger_1.logger.debug(`Skipping existing index: ${JSON.stringify(index)}`);
  68. }
  69. else {
  70. logger_1.logger.debug(`Creating new index: ${JSON.stringify(index)}`);
  71. await this.createIndex(options.project, index);
  72. }
  73. }
  74. if (shouldDeleteIndexes && indexesToDelete.length > 0) {
  75. utils.logLabeledBullet("firestore", `Deleting ${indexesToDelete.length} indexes...`);
  76. for (const index of indexesToDelete) {
  77. await this.deleteIndex(index);
  78. }
  79. }
  80. let shouldDeleteFields = options.force;
  81. if (fieldOverridesToDelete.length > 0) {
  82. if (options.nonInteractive && !options.force) {
  83. utils.logLabeledBullet("firestore", `there are ${fieldOverridesToDelete.length} field overrides defined in your project that are not present in your ` +
  84. "firestore indexes file. To delete them, run this command with the --force flag.");
  85. }
  86. else if (!options.force) {
  87. const indexesString = fieldOverridesToDelete
  88. .map((x) => this.prettyFieldString(x))
  89. .join("\n\t");
  90. utils.logLabeledBullet("firestore", `The following field overrides are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`);
  91. }
  92. if (!shouldDeleteFields) {
  93. shouldDeleteFields = await (0, prompt_1.promptOnce)({
  94. type: "confirm",
  95. name: "confirm",
  96. default: false,
  97. message: "Would you like to delete these field overrides? Selecting no will continue the rest of the deployment.",
  98. });
  99. }
  100. }
  101. const sortedFieldOverridesToDeploy = fieldOverridesToDeploy.sort(sort.compareFieldOverride);
  102. for (const field of sortedFieldOverridesToDeploy) {
  103. const exists = existingFieldOverrides.some((x) => this.fieldMatchesSpec(x, field));
  104. if (exists) {
  105. logger_1.logger.debug(`Skipping existing field override: ${JSON.stringify(field)}`);
  106. }
  107. else {
  108. logger_1.logger.debug(`Updating field override: ${JSON.stringify(field)}`);
  109. await this.patchField(options.project, field);
  110. }
  111. }
  112. if (shouldDeleteFields && fieldOverridesToDelete.length > 0) {
  113. utils.logLabeledBullet("firestore", `Deleting ${fieldOverridesToDelete.length} field overrides...`);
  114. for (const field of fieldOverridesToDelete) {
  115. await this.deleteField(field);
  116. }
  117. }
  118. }
  119. async listIndexes(project) {
  120. const url = `/projects/${project}/databases/(default)/collectionGroups/-/indexes`;
  121. const res = await this.apiClient.get(url);
  122. const indexes = res.body.indexes;
  123. if (!indexes) {
  124. return [];
  125. }
  126. return indexes.map((index) => {
  127. const fields = index.fields.filter((field) => {
  128. return field.fieldPath !== "__name__";
  129. });
  130. return {
  131. name: index.name,
  132. state: index.state,
  133. queryScope: index.queryScope,
  134. fields,
  135. };
  136. });
  137. }
  138. async listFieldOverrides(project) {
  139. const parent = `projects/${project}/databases/(default)/collectionGroups/-`;
  140. const url = `/${parent}/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:*`;
  141. const res = await this.apiClient.get(url);
  142. const fields = res.body.fields;
  143. if (!fields) {
  144. return [];
  145. }
  146. return fields.filter((field) => {
  147. return field.name.indexOf("__default__") < 0;
  148. });
  149. }
  150. makeIndexSpec(indexes, fields) {
  151. const indexesJson = indexes.map((index) => {
  152. return {
  153. collectionGroup: util.parseIndexName(index.name).collectionGroupId,
  154. queryScope: index.queryScope,
  155. fields: index.fields,
  156. };
  157. });
  158. if (!fields) {
  159. logger_1.logger.debug("No field overrides specified, using [].");
  160. fields = [];
  161. }
  162. const fieldsJson = fields.map((field) => {
  163. const parsedName = util.parseFieldName(field.name);
  164. const fieldIndexes = field.indexConfig.indexes || [];
  165. return {
  166. collectionGroup: parsedName.collectionGroupId,
  167. fieldPath: parsedName.fieldPath,
  168. ttl: !!field.ttlConfig,
  169. indexes: fieldIndexes.map((index) => {
  170. const firstField = index.fields[0];
  171. return {
  172. order: firstField.order,
  173. arrayConfig: firstField.arrayConfig,
  174. queryScope: index.queryScope,
  175. };
  176. }),
  177. };
  178. });
  179. const sortedIndexes = indexesJson.sort(sort.compareSpecIndex);
  180. const sortedFields = fieldsJson.sort(sort.compareFieldOverride);
  181. return {
  182. indexes: sortedIndexes,
  183. fieldOverrides: sortedFields,
  184. };
  185. }
  186. prettyPrintIndexes(indexes) {
  187. if (indexes.length === 0) {
  188. logger_1.logger.info("None");
  189. return;
  190. }
  191. const sortedIndexes = indexes.sort(sort.compareApiIndex);
  192. sortedIndexes.forEach((index) => {
  193. logger_1.logger.info(this.prettyIndexString(index));
  194. });
  195. }
  196. printFieldOverrides(fields) {
  197. if (fields.length === 0) {
  198. logger_1.logger.info("None");
  199. return;
  200. }
  201. const sortedFields = fields.sort(sort.compareApiField);
  202. sortedFields.forEach((field) => {
  203. logger_1.logger.info(this.prettyFieldString(field));
  204. });
  205. }
  206. validateSpec(spec) {
  207. validator.assertHas(spec, "indexes");
  208. spec.indexes.forEach((index) => {
  209. this.validateIndex(index);
  210. });
  211. if (spec.fieldOverrides) {
  212. spec.fieldOverrides.forEach((field) => {
  213. this.validateField(field);
  214. });
  215. }
  216. }
  217. validateIndex(index) {
  218. validator.assertHas(index, "collectionGroup");
  219. validator.assertHas(index, "queryScope");
  220. validator.assertEnum(index, "queryScope", Object.keys(API.QueryScope));
  221. validator.assertHas(index, "fields");
  222. index.fields.forEach((field) => {
  223. validator.assertHas(field, "fieldPath");
  224. validator.assertHasOneOf(field, ["order", "arrayConfig"]);
  225. if (field.order) {
  226. validator.assertEnum(field, "order", Object.keys(API.Order));
  227. }
  228. if (field.arrayConfig) {
  229. validator.assertEnum(field, "arrayConfig", Object.keys(API.ArrayConfig));
  230. }
  231. });
  232. }
  233. validateField(field) {
  234. validator.assertHas(field, "collectionGroup");
  235. validator.assertHas(field, "fieldPath");
  236. validator.assertHas(field, "indexes");
  237. if (typeof field.ttl !== "undefined") {
  238. validator.assertType("ttl", field.ttl, "boolean");
  239. }
  240. field.indexes.forEach((index) => {
  241. validator.assertHasOneOf(index, ["arrayConfig", "order"]);
  242. if (index.arrayConfig) {
  243. validator.assertEnum(index, "arrayConfig", Object.keys(API.ArrayConfig));
  244. }
  245. if (index.order) {
  246. validator.assertEnum(index, "order", Object.keys(API.Order));
  247. }
  248. if (index.queryScope) {
  249. validator.assertEnum(index, "queryScope", Object.keys(API.QueryScope));
  250. }
  251. });
  252. }
  253. async patchField(project, spec) {
  254. const url = `/projects/${project}/databases/(default)/collectionGroups/${spec.collectionGroup}/fields/${spec.fieldPath}`;
  255. const indexes = spec.indexes.map((index) => {
  256. return {
  257. queryScope: index.queryScope,
  258. fields: [
  259. {
  260. fieldPath: spec.fieldPath,
  261. arrayConfig: index.arrayConfig,
  262. order: index.order,
  263. },
  264. ],
  265. };
  266. });
  267. let data = {
  268. indexConfig: {
  269. indexes,
  270. },
  271. };
  272. if (spec.ttl) {
  273. data = Object.assign(data, {
  274. ttlConfig: {},
  275. });
  276. }
  277. if (typeof spec.ttl !== "undefined") {
  278. await this.apiClient.patch(url, data);
  279. }
  280. else {
  281. await this.apiClient.patch(url, data, { queryParams: { updateMask: "indexConfig" } });
  282. }
  283. }
  284. deleteField(field) {
  285. const url = field.name;
  286. const data = {};
  287. return this.apiClient.patch(`/${url}`, data);
  288. }
  289. createIndex(project, index) {
  290. const url = `/projects/${project}/databases/(default)/collectionGroups/${index.collectionGroup}/indexes`;
  291. return this.apiClient.post(url, {
  292. fields: index.fields,
  293. queryScope: index.queryScope,
  294. });
  295. }
  296. deleteIndex(index) {
  297. const url = index.name;
  298. return this.apiClient.delete(`/${url}`);
  299. }
  300. indexMatchesSpec(index, spec) {
  301. const collection = util.parseIndexName(index.name).collectionGroupId;
  302. if (collection !== spec.collectionGroup) {
  303. return false;
  304. }
  305. if (index.queryScope !== spec.queryScope) {
  306. return false;
  307. }
  308. if (index.fields.length !== spec.fields.length) {
  309. return false;
  310. }
  311. let i = 0;
  312. while (i < index.fields.length) {
  313. const iField = index.fields[i];
  314. const sField = spec.fields[i];
  315. if (iField.fieldPath !== sField.fieldPath) {
  316. return false;
  317. }
  318. if (iField.order !== sField.order) {
  319. return false;
  320. }
  321. if (iField.arrayConfig !== sField.arrayConfig) {
  322. return false;
  323. }
  324. i++;
  325. }
  326. return true;
  327. }
  328. fieldMatchesSpec(field, spec) {
  329. const parsedName = util.parseFieldName(field.name);
  330. if (parsedName.collectionGroupId !== spec.collectionGroup) {
  331. return false;
  332. }
  333. if (parsedName.fieldPath !== spec.fieldPath) {
  334. return false;
  335. }
  336. if (typeof spec.ttl !== "undefined" && util.booleanXOR(!!field.ttlConfig, spec.ttl)) {
  337. return false;
  338. }
  339. else if (!!field.ttlConfig && typeof spec.ttl === "undefined") {
  340. utils.logLabeledBullet("firestore", `there are TTL field overrides for collection ${spec.collectionGroup} defined in your project that are not present in your ` +
  341. "firestore indexes file. The TTL policy won't be deleted since is not specified as false.");
  342. }
  343. const fieldIndexes = field.indexConfig.indexes || [];
  344. if (fieldIndexes.length !== spec.indexes.length) {
  345. return false;
  346. }
  347. const fieldModes = fieldIndexes.map((index) => {
  348. const firstField = index.fields[0];
  349. return firstField.order || firstField.arrayConfig;
  350. });
  351. const specModes = spec.indexes.map((index) => {
  352. return index.order || index.arrayConfig;
  353. });
  354. for (const mode of fieldModes) {
  355. if (specModes.indexOf(mode) < 0) {
  356. return false;
  357. }
  358. }
  359. return true;
  360. }
  361. upgradeOldSpec(spec) {
  362. const result = {
  363. indexes: [],
  364. fieldOverrides: spec.fieldOverrides || [],
  365. };
  366. if (!(spec.indexes && spec.indexes.length > 0)) {
  367. return result;
  368. }
  369. if (spec.indexes[0].collectionId) {
  370. utils.logBullet(clc.bold(clc.cyan("firestore:")) +
  371. " your indexes indexes are specified in the v1beta1 API format. " +
  372. "Please upgrade to the new index API format by running " +
  373. clc.bold("firebase firestore:indexes") +
  374. " again and saving the result.");
  375. }
  376. result.indexes = spec.indexes.map((index) => {
  377. const i = {
  378. collectionGroup: index.collectionGroup || index.collectionId,
  379. queryScope: index.queryScope || API.QueryScope.COLLECTION,
  380. fields: [],
  381. };
  382. if (index.fields) {
  383. i.fields = index.fields.map((field) => {
  384. const f = {
  385. fieldPath: field.fieldPath,
  386. };
  387. if (field.order) {
  388. f.order = field.order;
  389. }
  390. else if (field.arrayConfig) {
  391. f.arrayConfig = field.arrayConfig;
  392. }
  393. else if (field.mode === API.Mode.ARRAY_CONTAINS) {
  394. f.arrayConfig = API.ArrayConfig.CONTAINS;
  395. }
  396. else {
  397. f.order = field.mode;
  398. }
  399. return f;
  400. });
  401. }
  402. return i;
  403. });
  404. return result;
  405. }
  406. prettyIndexString(index, includeState = true) {
  407. let result = "";
  408. if (index.state && includeState) {
  409. const stateMsg = `[${index.state}] `;
  410. if (index.state === API.State.READY) {
  411. result += clc.green(stateMsg);
  412. }
  413. else if (index.state === API.State.CREATING) {
  414. result += clc.yellow(stateMsg);
  415. }
  416. else {
  417. result += clc.red(stateMsg);
  418. }
  419. }
  420. const nameInfo = util.parseIndexName(index.name);
  421. result += clc.cyan(`(${nameInfo.collectionGroupId})`);
  422. result += " -- ";
  423. index.fields.forEach((field) => {
  424. if (field.fieldPath === "__name__") {
  425. return;
  426. }
  427. const orderOrArrayConfig = field.order ? field.order : field.arrayConfig;
  428. result += `(${field.fieldPath},${orderOrArrayConfig}) `;
  429. });
  430. return result;
  431. }
  432. prettyFieldString(field) {
  433. let result = "";
  434. const parsedName = util.parseFieldName(field.name);
  435. result +=
  436. "[" +
  437. clc.cyan(parsedName.collectionGroupId) +
  438. "." +
  439. clc.yellow(parsedName.fieldPath) +
  440. "] --";
  441. const fieldIndexes = field.indexConfig.indexes || [];
  442. if (fieldIndexes.length > 0) {
  443. fieldIndexes.forEach((index) => {
  444. const firstField = index.fields[0];
  445. const mode = firstField.order || firstField.arrayConfig;
  446. result += ` (${mode})`;
  447. });
  448. }
  449. else {
  450. result += " (no indexes)";
  451. }
  452. const fieldTtl = field.ttlConfig;
  453. if (fieldTtl) {
  454. result += ` TTL(${fieldTtl.state})`;
  455. }
  456. return result;
  457. }
  458. }
  459. exports.FirestoreIndexes = FirestoreIndexes;