Skip to content

Commit

Permalink
Merge aa99210 into 8218447
Browse files Browse the repository at this point in the history
  • Loading branch information
samtstern committed Mar 15, 2019
2 parents 8218447 + aa99210 commit 27db316
Show file tree
Hide file tree
Showing 5 changed files with 450 additions and 70 deletions.
177 changes: 177 additions & 0 deletions src/firestore/indexes-sort.ts
@@ -0,0 +1,177 @@
import * as API from "./indexes-api";
import * as Spec from "./indexes-spec";
import * as util from "./util";

const QUERY_SCOPE_SEQUENCE = [
API.QueryScope.COLLECTION_GROUP,
API.QueryScope.COLLECTION,
undefined,
];

const ORDER_SEQUENCE = [API.Order.ASCENDING, API.Order.DESCENDING, undefined];

const ARRAY_CONFIG_SEQUENCE = [API.ArrayConfig.CONTAINS, undefined];

/**
* Compare two Index spec entries for sorting.
*
* Comparisons:
* 1) The collection group.
* 2) The query scope.
* 3) The fields list.
*/
export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number {
if (a.collectionGroup !== b.collectionGroup) {
return a.collectionGroup.localeCompare(b.collectionGroup);
}

if (a.queryScope !== b.queryScope) {
return compareQueryScope(a.queryScope, b.queryScope);
}

return compareArrays(a.fields, b.fields, compareIndexField);
}

/**
* Compare two Index api entries for sorting.
*
* Comparisons:
* 1) The collection group.
* 2) The query scope.
* 3) The fields list.
*/
export function compareApiIndex(a: API.Index, b: API.Index): number {
// When these indexes are used as part of a field override, the name is
// not always present or relevant.
if (a.name && b.name) {
const aName = util.parseIndexName(a.name);
const bName = util.parseIndexName(b.name);

if (aName.collectionGroupId !== bName.collectionGroupId) {
return aName.collectionGroupId.localeCompare(bName.collectionGroupId);
}
}

if (a.queryScope !== b.queryScope) {
return compareQueryScope(a.queryScope, b.queryScope);
}

return compareArrays(a.fields, b.fields, compareIndexField);
}

/**
* Compare two Field api entries for sorting.
*
* Comparisons:
* 1) The collection group.
* 2) The field path.
* 3) The indexes list in the config.
*/
export function compareApiField(a: API.Field, b: API.Field): number {
const aName = util.parseFieldName(a.name);
const bName = util.parseFieldName(b.name);

if (aName.collectionGroupId !== bName.collectionGroupId) {
return aName.collectionGroupId.localeCompare(bName.collectionGroupId);
}

if (aName.fieldPath !== bName.fieldPath) {
return aName.fieldPath.localeCompare(bName.fieldPath);
}

return compareArrays(a.indexConfig.indexes || [], b.indexConfig.indexes || [], compareApiIndex);
}

/**
* Compare two Field override specs for sorting.
*
* Comparisons:
* 1) The collection group.
* 2) The field path.
* 3) The list of indexes.
*/
export function compareFieldOverride(a: Spec.FieldOverride, b: Spec.FieldOverride): number {
if (a.collectionGroup !== b.collectionGroup) {
return a.collectionGroup.localeCompare(b.collectionGroup);
}

if (a.fieldPath !== b.fieldPath) {
return a.fieldPath.localeCompare(b.fieldPath);
}

return compareArrays(a.indexes, b.indexes, compareFieldIndex);
}

/**
* Compare two IndexField objects.
*
* Comparisons:
* 1) Field path.
* 2) Sort order (if it exists).
* 3) Array config (if it exists).
*/
function compareIndexField(a: API.IndexField, b: API.IndexField): number {
if (a.fieldPath !== b.fieldPath) {
return a.fieldPath.localeCompare(b.fieldPath);
}

if (a.order !== b.order) {
return compareOrder(a.order, b.order);
}

if (a.arrayConfig !== b.arrayConfig) {
return compareArrayConfig(a.arrayConfig, b.arrayConfig);
}

return 0;
}

function compareFieldIndex(a: Spec.FieldIndex, b: Spec.FieldIndex): number {
if (a.queryScope !== b.queryScope) {
return compareQueryScope(a.queryScope, b.queryScope);
}

if (a.order !== b.order) {
return compareOrder(a.order, b.order);
}

if (a.arrayConfig !== b.arrayConfig) {
return compareArrayConfig(a.arrayConfig, b.arrayConfig);
}

return 0;
}

function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number {
return QUERY_SCOPE_SEQUENCE.indexOf(a) - QUERY_SCOPE_SEQUENCE.indexOf(b);
}

function compareOrder(a?: API.Order, b?: API.Order): number {
return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b);
}

function compareArrayConfig(a?: API.ArrayConfig, b?: API.ArrayConfig): number {
return ARRAY_CONFIG_SEQUENCE.indexOf(a) - ARRAY_CONFIG_SEQUENCE.indexOf(b);
}

/**
* Compare two arrays of objects by first sorting each array, then
* looking for the first non-equal element and comparing them.
*
* If the shorter array is a perfect prefix of the longer array,
* then the shorter array is sorted first.
*/
function compareArrays<T>(a: T[], b: T[], fn: (x: T, y: T) => number): number {
const aSorted = a.sort(fn);
const bSorted = b.sort(fn);

const minFields = Math.min(aSorted.length, bSorted.length);
for (let i = 0; i < minFields; i++) {
const cmp = fn(aSorted[i], bSorted[i]);
if (cmp !== 0) {
return cmp;
}
}

return aSorted.length - bSorted.length;
}
6 changes: 3 additions & 3 deletions src/firestore/indexes-spec.ts
Expand Up @@ -23,14 +23,14 @@ export interface FieldOverride {
*/
export interface FieldIndex {
queryScope: API.QueryScope;
order: API.Order | undefined;
arrayConfig: API.ArrayConfig | undefined;
order?: API.Order;
arrayConfig?: API.ArrayConfig;
}

/**
* Specification for the JSON file that is used for index deployment,
*/
export interface IndexFile {
indexes: Index[];
fieldOverrides: FieldOverride[] | undefined;
fieldOverrides?: FieldOverride[];
}
81 changes: 16 additions & 65 deletions src/firestore/indexes.ts
@@ -1,31 +1,14 @@
import * as clc from "cli-color";

import * as api from "../api";
import * as FirebaseError from "../error";
import * as logger from "../logger";
import * as utils from "../utils";
import * as validator from "./validator";

import * as API from "./indexes-api";
import * as Spec from "./indexes-spec";

// projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/indexes/$INDEX_ID
const INDEX_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/indexes\/([^\/]*)/;

// projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/fields/$FIELD_ID
const FIELD_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/fields\/([^\/]*)/;

interface IndexName {
projectId: string;
collectionGroupId: string;
indexId: string;
}

interface FieldName {
projectId: string;
collectionGroupId: string;
fieldPath: string;
}
import * as sort from "./indexes-sort";
import * as util from "./util";

export class FirestoreIndexes {
/**
Expand Down Expand Up @@ -157,7 +140,7 @@ export class FirestoreIndexes {
makeIndexSpec(indexes: API.Index[], fields?: API.Field[]): Spec.IndexFile {
const indexesJson = indexes.map((index) => {
return {
collectionGroup: this.parseIndexName(index.name).collectionGroupId,
collectionGroup: util.parseIndexName(index.name).collectionGroupId,
queryScope: index.queryScope,
fields: index.fields,
};
Expand All @@ -169,7 +152,7 @@ export class FirestoreIndexes {
}

const fieldsJson = fields.map((field) => {
const parsedName = this.parseFieldName(field.name);
const parsedName = util.parseFieldName(field.name);
const fieldIndexes = field.indexConfig.indexes || [];
return {
collectionGroup: parsedName.collectionGroupId,
Expand All @@ -186,9 +169,11 @@ export class FirestoreIndexes {
};
});

const sortedIndexes = indexesJson.sort(sort.compareSpecIndex);
const sortedFields = fieldsJson.sort(sort.compareFieldOverride);
return {
indexes: indexesJson,
fieldOverrides: fieldsJson,
indexes: sortedIndexes,
fieldOverrides: sortedFields,
};
}

Expand All @@ -202,7 +187,8 @@ export class FirestoreIndexes {
return;
}

indexes.forEach((index) => {
const sortedIndexes = indexes.sort(sort.compareApiIndex);
sortedIndexes.forEach((index) => {
logger.info(this.prettyIndexString(index));
});
}
Expand All @@ -217,7 +203,8 @@ export class FirestoreIndexes {
return;
}

fields.forEach((field) => {
const sortedFields = fields.sort(sort.compareApiField);
sortedFields.forEach((field) => {
logger.info(this.prettyFieldString(field));
});
}
Expand Down Expand Up @@ -349,7 +336,7 @@ export class FirestoreIndexes {
* Determine if an API Index and a Spec Index are functionally equivalent.
*/
indexMatchesSpec(index: API.Index, spec: Spec.Index): boolean {
const collection = this.parseIndexName(index.name).collectionGroupId;
const collection = util.parseIndexName(index.name).collectionGroupId;
if (collection !== spec.collectionGroup) {
return false;
}
Expand Down Expand Up @@ -389,7 +376,7 @@ export class FirestoreIndexes {
* Determine if an API Field and a Spec Field are functionally equivalent.
*/
fieldMatchesSpec(field: API.Field, spec: Spec.FieldOverride): boolean {
const parsedName = this.parseFieldName(field.name);
const parsedName = util.parseFieldName(field.name);

if (parsedName.collectionGroupId !== spec.collectionGroup) {
return false;
Expand Down Expand Up @@ -424,42 +411,6 @@ export class FirestoreIndexes {
return true;
}

/**
* Parse an Index name into useful pieces.
*/
parseIndexName(name?: string): IndexName {
if (!name) {
throw new FirebaseError(`Cannot parse undefined index name.`);
}

const m = name.match(INDEX_NAME_REGEX);
if (!m || m.length < 4) {
throw new FirebaseError(`Error parsing index name: ${name}`);
}

return {
projectId: m[1],
collectionGroupId: m[2],
indexId: m[3],
};
}

/**
* Parse an Field name into useful pieces.
*/
parseFieldName(name: string): FieldName {
const m = name.match(FIELD_NAME_REGEX);
if (!m || m.length < 4) {
throw new FirebaseError(`Error parsing field name: ${name}`);
}

return {
projectId: m[1],
collectionGroupId: m[2],
fieldPath: m[3],
};
}

/**
* Take a object that may represent an old v1beta1 indexes spec
* and convert it to the new v1beta2/v1 spec format.
Expand Down Expand Up @@ -539,7 +490,7 @@ export class FirestoreIndexes {
}
}

const nameInfo = this.parseIndexName(index.name);
const nameInfo = util.parseIndexName(index.name);

result += clc.cyan(`(${nameInfo.collectionGroupId})`);
result += " -- ";
Expand All @@ -564,7 +515,7 @@ export class FirestoreIndexes {
private prettyFieldString(field: API.Field): string {
let result = "";

const parsedName = this.parseFieldName(field.name);
const parsedName = util.parseFieldName(field.name);

result +=
"[" +
Expand Down

0 comments on commit 27db316

Please sign in to comment.