ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
573 lines (515 loc) • 14.3 kB
JavaScript
/*!
* 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.
*/
;
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};