Skip to content

Add setIndexConfiguration API #5843

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ export type { ByteString as _ByteString } from './util/byte_string';
export { logWarn as _logWarn } from './util/log';
export { EmptyAuthCredentialsProvider as _EmptyAuthCredentialsProvider } from './api/credentials';
export { EmptyAppCheckTokenProvider as _EmptyAppCheckTokenProvider } from './api/credentials';
export { setIndexConfiguration as _setIndexConfiguration } from './api/index_configuration';
23 changes: 14 additions & 9 deletions packages/firestore/src/api/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,13 @@ export function terminate(firestore: Firestore): Promise<void> {
/**
* Loads a Firestore bundle into the local cache.
*
* @param firestore - The {@link Firestore} instance to load bundles for for.
* @param bundleData - An object representing the bundle to be loaded. Valid objects are
* `ArrayBuffer`, `ReadableStream<Uint8Array>` or `string`.
* @param firestore - The {@link Firestore} instance to load bundles for.
* @param bundleData - An object representing the bundle to be loaded. Valid
* objects are `ArrayBuffer`, `ReadableStream<Uint8Array>` or `string`.
*
* @returns
* A `LoadBundleTask` object, which notifies callers with progress updates, and completion
* or error events. It can be used as a `Promise<LoadBundleTaskProgress>`.
* @returns A `LoadBundleTask` object, which notifies callers with progress
* updates, and completion or error events. It can be used as a
* `Promise<LoadBundleTaskProgress>`.
*/
export function loadBundle(
firestore: Firestore,
Expand All @@ -528,11 +528,16 @@ export function loadBundle(
}

/**
* Reads a Firestore {@link Query} from local cache, identified by the given name.
* Reads a Firestore {@link Query} from local cache, identified by the given
* name.
*
* The named queries are packaged into bundles on the server side (along
* with resulting documents), and loaded to local cache using `loadBundle`. Once in local
* cache, use this method to extract a {@link Query} by name.
* with resulting documents), and loaded to local cache using `loadBundle`. Once
* in local cache, use this method to extract a {@link Query} by name.
*
* @param firestore - The {@link Firestore} instance to read the query from.
* @param name - The name of the query.
* @returns A `Promise` that is resolved with the Query or `null`.
*/
export function namedQuery(
firestore: Firestore,
Expand Down
211 changes: 211 additions & 0 deletions packages/firestore/src/api/index_configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { fieldPathFromDotSeparatedString } from '../lite-api/user_data_reader';
import { FieldIndex, Kind, Segment } from '../model/field_index';
import { Code, FirestoreError } from '../util/error';
import { cast } from '../util/input_validation';

import { ensureFirestoreConfigured, Firestore } from './database';

export {
connectFirestoreEmulator,
EmulatorMockTokenOptions
} from '../lite-api/database';

// TODO(indexing): Remove "@internal" from the API.

/**
* A single field element in an index configuration.
*
* @internal
*/
export interface IndexField {
/** The field path to index. */
readonly fieldPath: string;
/**
* What type of array index to create. Set to `CONTAINS` for `array-contains`
* and `array-contains-any` indexes.
*
* Only one of `arrayConfig` or `order` should be set;
*/
readonly arrayConfig?: 'CONTAINS';
/**
* What type of array index to create. Set to `ASCENDING` or 'DESCENDING` for
* `==`, `!=`, `<=`, `<=`, `in` and `not-in` filters.
*
* Only one of `arrayConfig` or `order` should be set.
*/
readonly order?: 'ASCENDING' | 'DESCENDING';

[key: string]: unknown;
}

/**
* The SDK definition of a Firestore index.
*
* @internal
*/
export interface Index {
/** The ID of the collection to index. */
readonly collectionGroup: string;
/** A list of fields to index. */
readonly fields?: IndexField[];

[key: string]: unknown;
}

/**
* A list of Firestore indexes to speed up local query execution.
*
* See {@link https://firebase.google.com/docs/reference/firestore/indexes/#json_format | JSON Format}
* for a description of the format of the index definition.
*
* @internal
*/
export interface IndexConfiguration {
/** A list of all Firestore indexes. */
readonly indexes?: Index[];

[key: string]: unknown;
}

/**
* Configures indexing for local query execution. Any previous index
* configuration is overridden. The `Promise` resolves once the index
* configuration has been persisted.
*
* The index entries themselves are created asynchronously. You can continue to
* use queries that require indexing even if the indices are not yet available.
* Query execution will automatically start using the index once the index
* entries have been written.
*
* Indexes are only supported with IndexedDb persistence. Invoke either
* `enableIndexedDbPersistence()` or `enableMultiTabIndexedDbPersistence()`
* before setting an index configuration. If IndexedDb is not enabled, any
* index configuration is ignored.
*
* @internal
* @param firestore - The {@link Firestore} instance to configure indexes for.
* @param configuration -The index definition.
* @throws FirestoreError if the JSON format is invalid.
* @returns A `Promise` that resolves once all indices are successfully
* configured.
*/
export function setIndexConfiguration(
firestore: Firestore,
configuration: IndexConfiguration
): Promise<void>;
/**
* Configures indexing for local query execution. Any previous index
* configuration is overridden. The `Promise` resolves once the index
* configuration has been persisted.
*
* The index entries themselves are created asynchronously. You can continue to
* use queries that require indexing even if the indices are not yet available.
* Query execution will automatically start using the index once the index
* entries have been written.
*
* Indexes are only supported with IndexedDb persistence. Invoke either
* `enableIndexedDbPersistence()` or `enableMultiTabIndexedDbPersistence()`
* before setting an index configuration. If IndexedDb is not enabled, any
* index configuration is ignored.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the user sets index config without persistence enabled, then enables persistence afterwards? The current implementation still persists the index configs, but I think it'd be helpful to clarify what 'ignored' means in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not possible - setIndexConfiguration() requires an initialized client and persistence cannot be enabled afterwards.

*
* The method accepts the JSON format exported by the Firebase CLI (`firebase
* firestore:indexes`). If the JSON format is invalid, this method throws an
* error.
*
* @internal
* @param firestore - The {@link Firestore} instance to configure indexes for.
* @param json -The JSON format exported by the Firebase CLI.
* @throws FirestoreError if the JSON format is invalid.
* @returns A `Promise` that resolves once all indices are successfully
* configured.
*/
export function setIndexConfiguration(
firestore: Firestore,
json: string
): Promise<void>;
export function setIndexConfiguration(
firestore: Firestore,
jsonOrConfiguration: string | IndexConfiguration
): Promise<void> {
firestore = cast(firestore, Firestore);
ensureFirestoreConfigured(firestore);

const indexConfiguration =
typeof jsonOrConfiguration === 'string'
? (tryParseJson(jsonOrConfiguration) as IndexConfiguration)
: jsonOrConfiguration;
const parsedIndexes: FieldIndex[] = [];

// PORTING NOTE: We don't return an error if the user has not enabled
// persistence since `enableIndexeddbPersistence()` can fail on the Web.

if (Array.isArray(indexConfiguration.indexes)) {
for (const index of indexConfiguration.indexes) {
const collectionGroup = tryGetString(index, 'collectionGroup');

const segments: Segment[] = [];
if (Array.isArray(index.fields)) {
for (const field of index.fields) {
const fieldPathString = tryGetString(field, 'fieldPath');
const fieldPath = fieldPathFromDotSeparatedString(
'setIndexConfiguration',
fieldPathString
);

if (field.arrayConfig === 'CONTAINS') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider validating that only one of arrayConfig or order is set and throw an error if both are set / include test for 'CONTAINS'?

The Android SDK doesn't validate, but we also don't allow directly setting indexes with key-value pairs there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel there are lots of ways we can validate here, and it is a bit of a rabbit hole that I don't want to go down just yet. If there is demand, then we can consider.

segments.push(new Segment(fieldPath, Kind.CONTAINS));
} else if (field.order === 'ASCENDING') {
segments.push(new Segment(fieldPath, Kind.ASCENDING));
} else if (field.order === 'DESCENDING') {
segments.push(new Segment(fieldPath, Kind.DESCENDING));
}
}
}

parsedIndexes.push(
new FieldIndex(FieldIndex.UNKNOWN_ID, collectionGroup, segments)
);
}
}

// TODO(indexing): Configure indexes
return Promise.resolve();
}

function tryParseJson(json: string): Record<string, unknown> {
try {
return JSON.parse(json);
} catch (e) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Failed to parse JSON:' + e.message
);
}
}

function tryGetString(data: Record<string, unknown>, property: string): string {
if (typeof data[property] !== 'string') {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Missing string value for: ' + property
);
}
return data[property] as string;
}
70 changes: 70 additions & 0 deletions packages/firestore/src/model/field_index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { FieldPath } from './path';

/**
* An index definition for field indexes in Firestore.
*
* Every index is associated with a collection. The definition contains a list
* of fields and their index kind (which can be `ASCENDING`, `DESCENDING` or
* `CONTAINS` for ArrayContains/ArrayContainsAny queries).
*
* Unlike the backend, the SDK does not differentiate between collection or
* collection group-scoped indices. Every index can be used for both single
* collection and collection group queries.
*/
export class FieldIndex {
/** An ID for an index that has not yet been added to persistence. */
static UNKNOWN_ID: -1;

constructor(
/**
* The index ID. Returns -1 if the index ID is not available (e.g. the index
* has not yet been persisted).
*/
readonly indexId: number,
/** The collection ID this index applies to. */
readonly collectionGroup: string,
/** The field segments for this index. */
readonly segments: Segment[]
) {}
}

/** The type of the index, e.g. for which type of query it can be used. */
export const enum Kind {
/**
* Ordered index. Can be used for <, <=, ==, >=, >, !=, IN and NOT IN queries.
*/
ASCENDING,
/**
* Ordered index. Can be used for <, <=, ==, >=, >, !=, IN and NOT IN queries.
*/
DESCENDING,
/** Contains index. Can be used for ArrayContains and ArrayContainsAny. */
CONTAINS
}

/** An index component consisting of field path and index type. */
export class Segment {
constructor(
/** The field path of the component. */
readonly fieldPath: FieldPath,
/** The fields sorting order. */
readonly kind: Kind
) {}
}
Loading