"use strict";
/*!
* Copyright 2017 Google Inc. All Rights Reserved.
*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
const validate_1 = require("./validate");
/*!
* The default database ID for this Firestore client. We do not yet expose the
* ability to use different databases.
*/
exports.DEFAULT_DATABASE_ID = '(default)';
/*!
* A regular expression to verify an absolute Resource Path in Firestore. It
* extracts the project ID, the database name and the relative resource path
* if available.
*
* @type {RegExp}
*/
const RESOURCE_PATH_RE =
// Note: [\s\S] matches all characters including newlines.
/^projects\/([^/]*)\/databases\/([^/]*)(?:\/documents\/)?([\s\S]*)$/;
/*!
* A regular expression to verify whether a field name can be passed to the
* backend without escaping.
*
* @type {RegExp}
*/
const UNESCAPED_FIELD_NAME_RE = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
/*!
* A regular expression to verify field paths that are passed to the API as
* strings. Field paths that do not match this expression have to be provided
* as a [FieldPath]{@link FieldPath} object.
*
* @type {RegExp}
*/
const FIELD_PATH_RE = /^[^*~/[\]]+$/;
/**
* An abstract class representing a Firestore path.
*
* Subclasses have to implement `split()` and `canonicalString()`.
*
* @private
* @class
*/
class Path {
/**
* Creates a new Path with the given segments.
*
* @private
* @hideconstructor
* @param segments Sequence of parts of a path.
*/
constructor(segments) {
this.segments = segments;
}
/**
* Returns the number of segments of this field path.
*
* @private
*/
get size() {
return this.segments.length;
}
/**
* Create a child path beneath the current level.
*
* @private
* @param relativePath Relative path to append to the current path.
* @returns The new path.
*/
append(relativePath) {
if (relativePath instanceof Path) {
return this.construct(this.segments.concat(relativePath.segments));
}
return this.construct(this.segments.concat(this.split(relativePath)));
}
/**
* Returns the path of the parent node.
*
* @private
* @returns The new path or null if we are already at the root.
*/
parent() {
if (this.segments.length === 0) {
return null;
}
return this.construct(this.segments.slice(0, this.segments.length - 1));
}
/**
* Checks whether the current path is a prefix of the specified path.
*
* @private
* @param other The path to check against.
* @returns 'true' iff the current path is a prefix match with 'other'.
*/
isPrefixOf(other) {
if (other.segments.length < this.segments.length) {
return false;
}
for (let i = 0; i < this.segments.length; i++) {
if (this.segments[i] !== other.segments[i]) {
return false;
}
}
return true;
}
/**
* Compare the current path against another Path object.
*
* @private
* @param other The path to compare to.
* @returns -1 if current < other, 1 if current > other, 0 if equal
*/
compareTo(other) {
const len = Math.min(this.segments.length, other.segments.length);
for (let i = 0; i < len; i++) {
if (this.segments[i] < other.segments[i]) {
return -1;
}
if (this.segments[i] > other.segments[i]) {
return 1;
}
}
if (this.segments.length < other.segments.length) {
return -1;
}
if (this.segments.length > other.segments.length) {
return 1;
}
return 0;
}
/**
* Returns a copy of the underlying segments.
*
* @private
* @returns A copy of the segments that make up this path.
*/
toArray() {
return this.segments.slice();
}
/**
* Returns true if this `Path` is equal to the provided value.
*
* @private
* @param other The value to compare against.
* @return true if this `Path` is equal to the provided value.
*/
isEqual(other) {
return (this === other ||
(other instanceof this.constructor && this.compareTo(other) === 0));
}
}
/**
* A slash-separated path for navigating resources within the current Firestore
* instance.
*
* @private
*/
class ResourcePath extends Path {
/**
* Constructs a ResourcePath.
*
* @private
* @param segments Sequence of names of the parts of the path.
*/
constructor(...segments) {
super(segments);
}
/**
* Indicates whether this path points to a document.
* @private
*/
get isDocument() {
return this.segments.length > 0 && this.segments.length % 2 === 0;
}
/**
* Indicates whether this path points to a collection.
* @private
*/
get isCollection() {
return this.segments.length % 2 === 1;
}
/**
* The last component of the path.
* @private
*/
get id() {
if (this.segments.length > 0) {
return this.segments[this.segments.length - 1];
}
return null;
}
/**
* Returns the location of this path relative to the root of the project's
* database.
* @private
*/
get relativeName() {
return this.segments.join('/');
}
/**
* Constructs a new instance of ResourcePath.
*
* @private
* @param segments Sequence of parts of the path.
* @returns The newly created ResourcePath.
*/
construct(segments) {
return new ResourcePath(...segments);
}
/**
* Splits a string into path segments, using slashes as separators.
*
* @private
* @param relativePath The path to split.
* @returns The split path segments.
*/
split(relativePath) {
// We may have an empty segment at the beginning or end if they had a
// leading or trailing slash (which we allow).
return relativePath.split('/').filter(segment => segment.length > 0);
}
/**
* Converts this path to a fully qualified ResourcePath.
*
* @private
* @param projectIdIfMissing The project ID of the current Firestore project.
* The project ID is only used if it's not provided as part of this
* ResourcePath.
* @return A fully-qualified resource path pointing to the same element.
*/
toQualifiedResourcePath(projectIdIfMissing) {
return new QualifiedResourcePath(projectIdIfMissing, exports.DEFAULT_DATABASE_ID, ...this.segments);
}
}
exports.ResourcePath = ResourcePath;
/**
* A default instance pointing to the root collection.
* @private
*/
ResourcePath.EMPTY = new ResourcePath();
/**
* A slash-separated path that includes a project and database ID for referring
* to resources in any Firestore project.
*
* @private
*/
class QualifiedResourcePath extends ResourcePath {
/**
* Constructs a Firestore Resource Path.
*
* @private
* @param projectId The Firestore project id.
* @param databaseId The Firestore database id.
* @param segments Sequence of names of the parts of the path.
*/
constructor(projectId, databaseId, ...segments) {
super(...segments);
this.projectId = projectId;
this.databaseId = databaseId;
}
/**
* String representation of the path relative to the database root.
* @private
*/
get relativeName() {
return this.segments.join('/');
}
/**
* Creates a resource path from an absolute Firestore path.
*
* @private
* @param absolutePath A string representation of a Resource Path.
* @returns The new ResourcePath.
*/
static fromSlashSeparatedString(absolutePath) {
const elements = RESOURCE_PATH_RE.exec(absolutePath);
if (elements) {
const project = elements[1];
const database = elements[2];
const path = elements[3];
return new QualifiedResourcePath(project, database).append(path);
}
throw new Error(`Resource name '${absolutePath}' is not valid.`);
}
/**
* Create a child path beneath the current level.
*
* @private
* @param relativePath Relative path to append to the current path.
* @returns The new path.
*/
append(relativePath) {
// `super.append()` calls `QualifiedResourcePath.construct()` when invoked
// from here and returns a QualifiedResourcePath.
return super.append(relativePath);
}
/**
* Create a child path beneath the current level.
*
* @private
* @returns The new path.
*/
parent() {
return super.parent();
}
/**
* String representation of a ResourcePath as expected by the API.
*
* @private
* @returns The representation as expected by the API.
*/
get formattedName() {
const components = [
'projects',
this.projectId,
'databases',
this.databaseId,
'documents',
...this.segments,
];
return components.join('/');
}
/**
* Constructs a new instance of ResourcePath. We need this instead of using
* the normal constructor because polymorphic 'this' doesn't work on static
* methods.
*
* @private
* @param segments Sequence of names of the parts of the path.
* @returns The newly created QualifiedResourcePath.
*/
construct(segments) {
return new QualifiedResourcePath(this.projectId, this.databaseId, ...segments);
}
/**
* Convenience method to match the ResourcePath API. This method always
* returns the current instance. The arguments is ignored.
*
* @param projectIdIfMissing The project ID of the current Firestore project.
* The project ID is only used if it's not provided as part of this
* ResourcePath.
* @private
*/
toQualifiedResourcePath(projectIdIfMissing) {
return this;
}
/**
* Compare the current path against another ResourcePath object.
*
* @private
* @param other The path to compare to.
* @returns -1 if current < other, 1 if current > other, 0 if equal
*/
compareTo(other) {
if (other instanceof QualifiedResourcePath) {
if (this.projectId < other.projectId) {
return -1;
}
if (this.projectId > other.projectId) {
return 1;
}
if (this.databaseId < other.databaseId) {
return -1;
}
if (this.databaseId > other.databaseId) {
return 1;
}
}
return super.compareTo(other);
}
/**
* Converts this ResourcePath to the Firestore Proto representation.
* @private
*/
toProto() {
return {
referenceValue: this.formattedName,
};
}
}
exports.QualifiedResourcePath = QualifiedResourcePath;
/**
* Validates that the given string can be used as a relative or absolute
* resource path.
*
* @private
* @param arg The argument name or argument index (for varargs methods).
* @param resourcePath The path to validate.
* @throws if the string can't be used as a resource path.
*/
function validateResourcePath(arg, resourcePath) {
if (typeof resourcePath !== 'string' || resourcePath === '') {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'resource path')} Path must be a non-empty string.`);
}
if (resourcePath.indexOf('//') >= 0) {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'resource path')} Paths must not contain //.`);
}
}
exports.validateResourcePath = validateResourcePath;
/**
* A dot-separated path for navigating sub-objects within a document.
*
* @class
*/
class FieldPath extends Path {
/**
* Constructs a Firestore Field Path.
*
* @param {...string} segments Sequence of field names that form this path.
*
* @example
* let query = firestore.collection('col');
* let fieldPath = new FieldPath('f.o.o', 'bar');
*
* query.where(fieldPath, '==', 42).get().then(snapshot => {
* snapshot.forEach(document => {
* console.log(`Document contains {'f.o.o' : {'bar' : 42}}`);
* });
* });
*/
constructor(...segments) {
if (Array.isArray(segments[0])) {
throw new Error('The FieldPath constructor no longer supports an array as its first argument. ' +
'Please unpack your array and call FieldPath() with individual arguments.');
}
validate_1.validateMinNumberOfArguments('FieldPath', segments, 1);
for (let i = 0; i < segments.length; ++i) {
validate_1.validateString(i, segments[i]);
if (segments[i].length === 0) {
throw new Error(`Element at index ${i} should not be an empty string.`);
}
}
super(segments);
}
/**
* A special FieldPath value to refer to the ID of a document. It can be used
* in queries to sort or filter by the document ID.
*
* @returns {FieldPath}
*/
static documentId() {
return FieldPath._DOCUMENT_ID;
}
/**
* Turns a field path argument into a [FieldPath]{@link FieldPath}.
* Supports FieldPaths as input (which are passed through) and dot-separated
* strings.
*
* @private
* @param {string|FieldPath} fieldPath The FieldPath to create.
* @returns {FieldPath} A field path representation.
*/
static fromArgument(fieldPath) {
// validateFieldPath() is used in all public API entry points to validate
// that fromArgument() is only called with a Field Path or a string.
return fieldPath instanceof FieldPath
? fieldPath
: new FieldPath(...fieldPath.split('.'));
}
/**
* String representation of a FieldPath as expected by the API.
*
* @private
* @override
* @returns {string} The representation as expected by the API.
*/
get formattedName() {
return this.segments
.map(str => {
return UNESCAPED_FIELD_NAME_RE.test(str)
? str
: '`' + str.replace('\\', '\\\\').replace('`', '\\`') + '`';
})
.join('.');
}
/**
* Returns a string representation of this path.
*
* @private
* @returns A string representing this path.
*/
toString() {
return this.formattedName;
}
/**
* Splits a string into path segments, using dots as separators.
*
* @private
* @override
* @param {string} fieldPath The path to split.
* @returns {Array.<string>} - The split path segments.
*/
split(fieldPath) {
return fieldPath.split('.');
}
/**
* Constructs a new instance of FieldPath. We need this instead of using
* the normal constructor because polymorphic 'this' doesn't work on static
* methods.
*
* @private
* @override
* @param segments Sequence of field names.
* @returns The newly created FieldPath.
*/
construct(segments) {
return new FieldPath(...segments);
}
/**
* Returns true if this `FieldPath` is equal to the provided value.
*
* @param {*} other The value to compare against.
* @return {boolean} true if this `FieldPath` is equal to the provided value.
*/
isEqual(other) {
return super.isEqual(other);
}
}
exports.FieldPath = FieldPath;
/**
* A special sentinel value to refer to the ID of a document.
*
* @private
*/
FieldPath._DOCUMENT_ID = new FieldPath('__name__');
/**
* Validates that the provided value can be used as a field path argument.
*
* @private
* @param arg The argument name or argument index (for varargs methods).
* @param fieldPath The value to verify.
* @throws if the string can't be used as a field path.
*/
function validateFieldPath(arg, fieldPath) {
if (fieldPath instanceof FieldPath) {
return;
}
if (fieldPath === undefined) {
throw new Error(validate_1.invalidArgumentMessage(arg, 'field path') + ' The path cannot be omitted.');
}
if (util_1.isObject(fieldPath) && fieldPath.constructor.name === 'FieldPath') {
throw new Error(validate_1.customObjectMessage(arg, fieldPath));
}
if (typeof fieldPath !== 'string') {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'field path')} Paths can only be specified as strings or via a FieldPath object.`);
}
if (fieldPath.indexOf('..') >= 0) {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'field path')} Paths must not contain ".." in them.`);
}
if (fieldPath.startsWith('.') || fieldPath.endsWith('.')) {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'field path')} Paths must not start or end with ".".`);
}
if (!FIELD_PATH_RE.test(fieldPath)) {
throw new Error(`${validate_1.invalidArgumentMessage(arg, 'field path')} Paths can't be empty and must not contain
"*~/[]".`);
}
}
exports.validateFieldPath = validateFieldPath;
//# sourceMappingURL=path.js.map