UNPKG

ran-boilerplate

Version:

React . Apollo (GraphQL) . Next.js Toolkit

573 lines (515 loc) 14.3 kB
/*! * 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. */ 'use strict'; const is = require('is'); const validate = require('./validate')(); /*! * 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 = /^projects\/([^/]*)\/databases\/([^/]*)(?:\/documents\/)?(.*)$/; /*! * 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 {...string|string[]} segments - Sequence of parts of a path. */ constructor(segments) { segments = is.array(segments) ? segments : Array.prototype.slice.call(arguments); /** * @private */ this.segments = segments; /** * @private * @type {string|undefined} */ this._formattedName = undefined; } /** * String representation as expected by the proto API. * * @private * @type {string} */ get formattedName() { if (is.undefined(this._formattedName)) { this._formattedName = this.canonicalString(); } return this._formattedName; } /** * Create a child path beneath the current level. * * @private * @param {string|T} relativePath - Relative path to append to the current * path. * @returns {T} The new path. * @template T */ append(relativePath) { if (is.instanceof(relativePath, 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 {T|null} The new path or null if we are already at the root. * @returns {T} The new path. * @template T */ 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 {Path} other - The path to check against. * @returns {boolean} '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; } /** * Returns a string representation of this path. * * @private * @returns {string} A string representing this path. */ toString() { return this.formattedName; } /** * Compare the current path against another Path object. * * @private * @param {Path} other - The path to compare to. * @returns {number} -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 {Array.<string>} A copy of the segments that make up this path. */ toArray() { return this.segments.slice(); } } /** * A slash-separated path for navigating resources (documents and collections) * within Firestore. * * @private * @class */ class ResourcePath extends Path { /** * Constructs a Firestore Resource Path. * * @private * @hideconstructor * * @param {string} projectId - The Firestore project id. * @param {string} databaseId - The Firestore database id. * @param {...string|string[]} segments - Sequence of names of the parts of * the path. */ constructor(projectId, databaseId, segments) { segments = is.array(segments) ? segments : Array.prototype.slice.call(arguments, 2); super(segments); /** * @type {string} * @private */ this._projectId = projectId; /** * @type {string} * @private */ this._databaseId = databaseId; } /** * String representation of the path relative to the database root. * * @private * @type {string} */ get relativeName() { return this.segments.join('/'); } /** * Indicates whether this ResourcePath points to a document. * * @private * @type {boolean} */ get isDocument() { return this.segments.length > 0 && this.segments.length % 2 === 0; } /** * Indicates whether this ResourcePath points to a collection. * * @private * @type {boolean} */ get isCollection() { return this.segments.length % 2 === 1; } /** * The last component of the path. * * @private * @type {string|null} */ get id() { if (this.segments.length > 0) { return this.segments[this.segments.length - 1]; } return null; } /** * The project ID of this path. * * @private * @type {string} */ get projectId() { return this._projectId; } /** * The database ID of this path. * * @private * @type {string} */ get databaseId() { return this._databaseId; } /** * Returns true if the given string can be used as a relative or absolute * resource path. * * @private * @param {string} resourcePath - The path to validate. * @throws if the string can't be used as a resource path. * @returns {boolean} 'true' when the path is valid. */ static validateResourcePath(resourcePath) { if (!is.string(resourcePath) || resourcePath === '') { throw new Error(`Path must be a non-empty string.`); } if (resourcePath.indexOf('//') >= 0) { throw new Error('Paths must not contain //.'); } return true; } /** * Creates a resource path from an absolute Firestore path. * * @private * @param {string} absolutePath - A string representation of a Resource Path. * @returns {ResourcePath} The new ResourcePath. */ static fromSlashSeparatedString(absolutePath) { let elements = RESOURCE_PATH_RE.exec(absolutePath); if (elements) { const project = elements[1]; const database = elements[2]; const path = elements[3]; return new ResourcePath(project, database).append(path); } throw new Error(`Resource name '${absolutePath}' is not valid.`); } /** * Splits a string into path segments, using slashes as separators. * * @private * @override * @param {string} relativePath - The path to split. * @returns {Array.<string>} - 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); } /** * String representation of a ResourcePath as expected by the API. * * @private * @override * @returns {string} The representation as expected by the API. */ canonicalString() { let components = [ 'projects', this._projectId, 'databases', this._databaseId, ]; if (this.segments.length > 0) { components = components.concat('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 * @override * @param {Array.<string>} segments - Sequence of names of the parts of the * path. * @returns {ResourcePath} The newly created ResourcePath. */ construct(segments) { return new ResourcePath(this._projectId, this._databaseId, segments); } /** * Compare the current path against another ResourcePath object. * * @private * @override * @param {ResourcePath} other - The path to compare to. * @returns {number} -1 if current < other, 1 if current > other, 0 if equal */ compareTo(other) { // Ignore DocumentReference with {{projectId}} placeholders and assume that // the resolved IDs match the provided ResourcePath. We could alternatively // try to resolve the Project ID here, but this is asynchronous as it // requires Disk I/O. if ( this._projectId !== '{{projectId}}' && other._projectId !== '{{projectId}}' ) { 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); } } /** * A dot-separated path for navigating sub-objects within a document. * * @class */ class FieldPath extends Path { /** * Constructs a Firestore Field Path. * * @param {...string|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) { validate.minNumberOfArguments('FieldPath', arguments, 1); segments = is.array(segments) ? segments : Array.prototype.slice.call(arguments); for (let i = 0; i < segments.length; ++i) { validate.isString(i, segments[i]); if (segments[i].length === 0) { throw new Error(`Argument at index ${i} should not be empty.`); } } 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; } /** * Returns true if the provided value can be used as a field path argument. * * @private * @param {string|FieldPath} fieldPath - The value to verify. * @throws if the string can't be used as a field path. * @returns {boolean} 'true' when the path is valid. */ static validateFieldPath(fieldPath) { if (!is.instanceof(fieldPath, FieldPath)) { if (!is.string(fieldPath)) { throw new Error(`Paths must be strings or FieldPath objects.`); } if (fieldPath.indexOf('..') >= 0) { throw new Error(`Paths must not contain '..' in them.`); } if (fieldPath.startsWith('.') || fieldPath.endsWith('.')) { throw new Error(`Paths must not start or end with '.'.`); } if (!FIELD_PATH_RE.test(fieldPath)) { throw new Error(`Paths can't be empty and must not contain '*~/[]'.`); } } return true; } /** * Turns a field path argument into a [FieldPath]{@link FieldPath}. * Supports FieldPaths as input (which are passed through) and dot-seperated * 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. */ canonicalString() { return this.segments .map(str => { return UNESCAPED_FIELD_NAME_RE.test(str) ? str : '`' + str.replace('\\', '\\\\').replace('`', '\\`') + '`'; }) .join('.'); } /** * 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 {Array.<string>} segments - Sequence of field names. * @returns {ResourcePath} The newly created FieldPath. */ construct(segments) { return new FieldPath(segments); } } /** * A special sentinel value to refer to the ID of a document. * * @type {FieldPath} * @private */ FieldPath._DOCUMENT_ID = new FieldPath('__name__'); module.exports = {FieldPath, ResourcePath};