diff --git a/packages/mongodb-ns/src/index.spec.ts b/packages/mongodb-ns/src/index.spec.ts index a0b89ab37..5c0f74dc6 100644 --- a/packages/mongodb-ns/src/index.spec.ts +++ b/packages/mongodb-ns/src/index.spec.ts @@ -231,6 +231,28 @@ describe('ns', function () { assert.equal(ns('abc.').database, 'abc'); }); + describe('shared classification regexps', function () { + it('keeps an implied lastIndex of 0 between parses', function () { + // The RegExps were moved up to the module scope and are shared across invocations + // if one were to add 'g' or 'y' they would become stateful + // these tests will fail if the RegExps become stateful + assert(ns('__mdb_internal_a.x').isInternal()); + assert(ns('__mdb_internal_b.x').isInternal()); + + assert(ns('a.system.javascript').isSystem()); + assert(ns('b.system.typescript').isSystem()); + + assert(ns('c.local.oplog.rs').isOplog()); + assert(ns('d.local.oplog.$main').isOplog()); + + assert.equal(ns('foo').validDatabaseName, true); + assert.equal(ns('bar').validDatabaseName, true); + + assert(ns('p.mycol').validCollectionName); + assert(ns('q.yourcol').validCollectionName); + }); + }); + describe('sorting', function () { it('should sort them', function () { const names = [ diff --git a/packages/mongodb-ns/src/index.ts b/packages/mongodb-ns/src/index.ts index 0ac0aec72..a2fa020dd 100644 --- a/packages/mongodb-ns/src/index.ts +++ b/packages/mongodb-ns/src/index.ts @@ -1,26 +1,111 @@ +/** + * A MongoDB namespace (the `database.collection` string) and the + * classification flags derived from it. + */ type NS = { + /** + * The full namespace string, e.g. `'mydb.mycollection'`. + */ ns: string; + /** + * Index of the first `.` in `ns`, or `-1` when there is no dot. + */ dotIndex: number; + /** + * Everything before the first `.` in `ns` + * or the whole string when there is no dot. + */ database: string; + /** + * Everything after the first `.` in `ns`, or `''` when there is no dot. + */ collection: string; + /** + * @see isSystem + */ system: boolean; + /** + * Whether this is a system-generated collection: the collection name is a + * `system.*` collection (anything beginning with `system.` except + * `system.profile` itself) or an `enxcol_.` queryable-encryption + * collection. + */ isSystem(): boolean; + /** + * @see isOplog + */ oplog: boolean; + /** + * Whether the namespace is one of the two oplog namespaces, + * `local.oplog.$main` or `local.oplog.rs`. + */ isOplog(): boolean; + /** + * @see isCommand + */ command: boolean; + /** + * Whether the collection is the command collection `$cmd` or begins with + * `$cmd.sys`. + */ isCommand(): boolean; + /** + * @see isSpecial + */ special: boolean; + /** + * Whether the namespace is not an ordinary database/collection pair: set + * when any of `oplog`, `command`, `system`, `internal` is true or the + * database is `config`. + */ isSpecial(): boolean; + /** + * One level looser than `special`: true when `special` is true, or the + * database is `local` or `admin`. `NS.sort` uses it to keep these + * namespaces at the end of a sorted list. + */ specialish: boolean; + /** + * @see isNormal + */ normal: boolean; + /** + * Whether the namespace is an oplog, or when `ns` contains no `$` character + * at all. (Despite the name, this includes the oplog.) + */ isNormal(): boolean; + /** + * @see isInternal + */ internal: boolean; + /** + * Whether the database is an internal database - its name matches + * `/^__mdb_internal_\w/`. See the MongoDB Atlas "internal databases" docs. + * + * @see https://www.mongodb.com/docs/atlas/reference/internal-database/#internal-databases + */ isInternal(): boolean; + /** + * True when the database is a valid database name, i.e. it contains no + * backslash, `/`, `"`, `.` or space, and is at most + * `NS.MAX_DATABASE_NAME_LENGTH` (128) characters long. + */ validDatabaseName: boolean; + /** + * True when the collection name is non-empty and either the namespace is an + * oplog or the name contains no NUL byte and no `$`. + */ validCollectionName: boolean; + /** + * @deprecated An approximate equivalent to Java's hashCode that does not work for db names longer than 30 characters. + * + * Hash of the database portion of `ns`, computed in the constructor from + * the characters up to (and excluding) the first `.`. + */ databaseHash: number; + /** The namespace string itself (`ns`). */ toString(): string; - // Assigned in the constructor, but will always be undefined + /** Always `undefined` - reserved, never implemented. */ isConf(): undefined; }; @@ -32,6 +117,12 @@ type NSConstructor = { sort(namespaces: (string | NS)[]): typeof namespaces; }; +const INTERNAL_DATABASE_REGEXP = /^__mdb_internal_\w/; +const SYSTEM_COLLECTION_REGEXP = /^(?:system(?!\.profile$).*|enxcol_)\./; +const OPLOG_REGEXP = /local\.oplog\.(\$main|rs)/; +const VALID_DATABASE_NAME_REGEXP = /^[^\\/". ]*$/; +const VALID_COLLECTION_NAME_REGEXP = /^[^\0$]*$/; + // eslint-disable-next-line complexity const NS: NSConstructor = function (this: NS, ns: string | NS): NS { ns = ns.toString(); @@ -49,12 +140,11 @@ const NS: NSConstructor = function (this: NS, ns: string | NS): NS { this.collection = ns.slice(this.dotIndex + 1); } - // https://www.mongodb.com/docs/atlas/reference/internal-database/#internal-databases - this.internal = /^__mdb_internal_\w/.test(this.database); + this.internal = INTERNAL_DATABASE_REGEXP.test(this.database); - this.system = /^(?:system(?!\.profile$).*|enxcol_)\./.test(this.collection); + this.system = SYSTEM_COLLECTION_REGEXP.test(this.collection); - this.oplog = /local\.oplog\.(\$main|rs)/.test(ns); + this.oplog = OPLOG_REGEXP.test(ns); this.command = this.collection === '$cmd' || this.collection.indexOf('$cmd.sys') === 0; @@ -66,7 +156,7 @@ const NS: NSConstructor = function (this: NS, ns: string | NS): NS { this.internal; this.specialish = - this.special || ['local', 'admin'].indexOf(this.database) > -1; + this.special || this.database === 'local' || this.database === 'admin'; this.normal = this.oplog || this.ns.indexOf('$') === -1; @@ -75,11 +165,11 @@ const NS: NSConstructor = function (this: NS, ns: string | NS): NS { * `*<>:|?` */ this.validDatabaseName = - new RegExp('^[^\\\\/". ]*$').test(this.database) && + VALID_DATABASE_NAME_REGEXP.test(this.database) && this.database.length <= NS.MAX_DATABASE_NAME_LENGTH; this.validCollectionName = this.collection.length > 0 && - (this.oplog || /^[^\0$]*$/.test(this.collection)); + (this.oplog || VALID_COLLECTION_NAME_REGEXP.test(this.collection)); this.databaseHash = 7; this.ns.split('').every((c, i) => {