UNPKG

@datastax/astra-mongoose

Version:

Astra's NodeJS Mongoose compatibility client

452 lines 20.8 kB
"use strict"; // Copyright DataStax, Inc. // // 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. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OperationNotSupportedError = exports.Collection = void 0; const collection_1 = __importDefault(require("mongoose/lib/collection")); const astra_db_ts_1 = require("@datastax/astra-db-ts"); const deserializeDoc_1 = __importDefault(require("../deserializeDoc")); const util_1 = require("util"); const serialize_1 = require("../serialize"); const setDefaultIdForUpsert_1 = require("../setDefaultIdForUpsert"); /** * Collection operations supported by the driver. This class is called "Collection" for consistency with Mongoose, because * in Mongoose a Collection is the interface that Models and Queries use to communicate with the database. However, from * an Astra perspective, this class can be a wrapper around a Collection **or** a Table depending on the corresponding db's * `isTable` option. Needs to be a separate class because Mongoose only supports one collection class. */ class Collection extends collection_1.default { constructor(name, conn, options) { super(name, conn, options); this.debugType = 'AstraMongooseCollection'; this.connection = conn; this._closed = false; this.options = options; this.name = name; } // Get the collection or table. Cache the result so we don't recreate collection/table every time. get collection() { if (this._collection != null) { return this._collection; } const collectionOptions = this.options?.schemaUserProvidedOptions?.serdes // Type coercion because `collection<>` method below doesn't know whether we're creating a // Astra Table or Astra Collection until runtime ? { serdes: this.options.schemaUserProvidedOptions.serdes } : {}; // Cache because @datastax/astra-db-ts doesn't const collection = this.connection.db.collection(this.name, collectionOptions); this._collection = collection; return collection; } // Get whether the underlying Astra store is a table or a collection. `connection.db` may be `null` if // the connection has never been opened (`mongoose.connect()` or `openUri()` never called), so in that // case we default to `isTable: false`. get isTable() { return this.connection.db?.isTable; } /** * Count documents in the collection that match the given filter. * @param filter */ async countDocuments(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'countDocuments', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use countDocuments() with tables'); } filter = (0, serialize_1.serialize)(filter); return this.collection.countDocuments(filter, 1000, options); } /** * Find documents in the collection that match the given filter. * @param filter * @param options * @param callback */ find(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'find', arguments); const requestOptions = options != null && options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter, this.isTable); return this.collection.find(filter, requestOptions).map(doc => (0, deserializeDoc_1.default)(doc)); } /** * Find a single document in the collection that matches the given filter. * @param filter * @param options */ async findOne(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'findOne', arguments); const requestOptions = options != null && options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter, this.isTable); return this.collection.findOne(filter, requestOptions).then(doc => (0, deserializeDoc_1.default)(doc)); } /** * Insert a single document into the collection. * @param doc */ async insertOne(doc, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'insertOne', arguments); return this.collection.insertOne((0, serialize_1.serialize)(doc, this.isTable), options); } /** * Insert multiple documents into the collection. * @param documents * @param options */ async insertMany(documents, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'insertMany', arguments); documents = documents.map(doc => (0, serialize_1.serialize)(doc, this.isTable)); return this.collection.insertMany(documents, options); } /** * Update a single document in a collection. * @param filter * @param update * @param options */ async findOneAndUpdate(filter, update, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'findOneAndUpdate', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use findOneAndUpdate() with tables'); } const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter); (0, setDefaultIdForUpsert_1.setDefaultIdForUpdate)(filter, update, requestOptions); update = (0, serialize_1.serialize)(update); return this.collection.findOneAndUpdate(filter, update, requestOptions).then((value) => { if (options?.includeResultMetadata) { return { value: (0, deserializeDoc_1.default)(value) }; } return (0, deserializeDoc_1.default)(value); }); } /** * Find a single document in the collection and delete it. * @param filter * @param options */ async findOneAndDelete(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'findOneAndDelete', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use findOneAndDelete() with tables'); } const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter); return this.collection.findOneAndDelete(filter, requestOptions).then((value) => { if (options?.includeResultMetadata) { return { value: (0, deserializeDoc_1.default)(value) }; } return (0, deserializeDoc_1.default)(value); }); } /** * Find a single document in the collection and replace it. * @param filter * @param newDoc * @param options */ async findOneAndReplace(filter, newDoc, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'findOneAndReplace', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use findOneAndReplace() with tables'); } const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter); (0, setDefaultIdForUpsert_1.setDefaultIdForReplace)(filter, newDoc, requestOptions); newDoc = (0, serialize_1.serialize)(newDoc); return this.collection.findOneAndReplace(filter, newDoc, requestOptions).then((value) => { if (options?.includeResultMetadata) { return { value: (0, deserializeDoc_1.default)(value) }; } return (0, deserializeDoc_1.default)(value); }); } /** * Delete one or more documents in a collection that match the given filter. * @param filter */ async deleteMany(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'deleteMany', arguments); filter = (0, serialize_1.serialize)(filter, this.isTable); return this.collection.deleteMany(filter, options); } /** * Delete a single document in a collection that matches the given filter. * @param filter * @param options * @param callback */ async deleteOne(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'deleteOne', arguments); const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter, this.isTable); return this.collection.deleteOne(filter, requestOptions); } /** * Update a single document in a collection that matches the given filter, replacing it with `replacement`. * Converted to a `findOneAndReplace()` under the hood. * @param filter * @param replacement * @param options */ async replaceOne(filter, replacement, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'replaceOne', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use replaceOne() with tables'); } const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter); (0, setDefaultIdForUpsert_1.setDefaultIdForReplace)(filter, replacement, requestOptions); replacement = (0, serialize_1.serialize)(replacement); return this.collection.replaceOne(filter, replacement, requestOptions); } /** * Update a single document in a collection that matches the given filter. * @param filter * @param update * @param options */ async updateOne(filter, update, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'updateOne', arguments); const requestOptions = options.sort != null ? { ...options, sort: processSortOption(options.sort) } : { ...options, sort: undefined }; filter = (0, serialize_1.serialize)(filter, this.isTable); // `setDefaultIdForUpdate` currently would not work with tables because tables don't support `$setOnInsert`. // But `setDefaultIdForUpdate` would also never use `$setOnInsert` if `updateOne` is used correctly because tables // require `_id` in filter. So safe to avoid this for tables. if (!this.isTable) { (0, setDefaultIdForUpsert_1.setDefaultIdForUpdate)(filter, update, requestOptions); } update = (0, serialize_1.serialize)(update, this.isTable); return this.collection.updateOne(filter, update, requestOptions).then(res => { // Mongoose currently has a bug where null response from updateOne() throws an error that we can't // catch here for unknown reasons. See Automattic/mongoose#15126. Tables API returns null here. return res ?? {}; }); } /** * Update multiple documents in a collection that match the given filter. * @param filter * @param update * @param options */ async updateMany(filter, update, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'updateMany', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use updateMany() with tables'); } filter = (0, serialize_1.serialize)(filter, this.isTable); (0, setDefaultIdForUpsert_1.setDefaultIdForUpdate)(filter, update, options); update = (0, serialize_1.serialize)(update, this.isTable); return this.collection.updateMany(filter, update, options); } /** * Get the estimated number of documents in a collection based on collection metadata */ async estimatedDocumentCount(options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'estimatedDocumentCount', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use estimatedDocumentCount() with tables'); } return this.collection.estimatedDocumentCount(options); } /** * Run an arbitrary command against this collection * @param command */ async runCommand(command, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'runCommand', arguments); return this.connection.db.astraDb.command(command, this.isTable ? { table: this.name, ...options } : { collection: this.name, ...options }); } /** * Bulk write not supported. * @param ops * @param options */ bulkWrite() { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'bulkWrite', arguments); throw new OperationNotSupportedError('bulkWrite() Not Implemented'); } /** * Aggregate not supported. * @param pipeline * @param options */ aggregate() { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'aggregate', arguments); throw new OperationNotSupportedError('aggregate() Not Implemented'); } /** * Returns a list of all indexes on the collection. Returns a pseudo-cursor for Mongoose compatibility. * Only works in tables mode, throws an error in collections mode. */ listIndexes() { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'listIndexes', arguments); if (this.collection instanceof astra_db_ts_1.Collection) { throw new OperationNotSupportedError('Cannot use listIndexes() with collections'); } /** * Mongoose expects listIndexes() to return a cursor but Astra returns an array. Mongoose itself doesn't support * returning a cursor from Model.listIndexes(), so all we need to return is an object with a toArray() function. */ return { toArray: () => this.runCommand({ listIndexes: { options: { explain: true } } }) .then(res => { const indexes = res.status.indexes; // Mongoose uses the `key` property of an index for index diffing in `cleanIndexes()` and `syncIndexes()`. return indexes.map((index) => ({ ...index, key: typeof index.definition.column === 'string' ? { [index.definition.column]: 1 } : index.definition.column })); }) }; } async createIndex(indexSpec, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'createIndex', arguments); if (this.collection instanceof astra_db_ts_1.Collection) { throw new OperationNotSupportedError('Cannot use createIndex() with collections'); } if (Object.keys(indexSpec).length !== 1) { throw new TypeError('createIndex indexSpec must have exactly 1 key'); } const [column] = Object.keys(indexSpec); if (options?.vector) { return this.collection.createVectorIndex(options?.name ?? column, column, { ifNotExists: true, ...options }); } return this.collection.createIndex(options?.name ?? column, indexSpec[column] === '$keys' || indexSpec[column] === '$values' ? { [column]: indexSpec[column] } : column, { ifNotExists: true, ...options }); } /** * Drop an existing index by name. Only works in tables mode, throws an error in collections mode. * * @param name */ async dropIndex(name, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'dropIndex', arguments); if (this.collection instanceof astra_db_ts_1.Collection) { throw new OperationNotSupportedError('Cannot use dropIndex() with collections'); } await this.connection.db.astraDb.dropTableIndex(name, options); } /** * Finds documents that match the filter and reranks them based on the provided options. * @param filter * @param options */ async findAndRerank(filter, options) { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'findAndRerank', arguments); if (this.collection instanceof astra_db_ts_1.Table) { throw new OperationNotSupportedError('Cannot use findAndRerank() with tables'); } return this.collection.findAndRerank(filter, options); } /** * Watch operation not supported. * * @ignore */ watch() { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'watch', arguments); throw new OperationNotSupportedError('watch() Not Implemented'); } /** * Distinct operation not supported. * * @ignore */ distinct() { // eslint-disable-next-line prefer-rest-params _logFunctionCall(this.connection.debug, this.name, 'distinct', arguments); throw new OperationNotSupportedError('distinct() Not Implemented'); } } exports.Collection = Collection; function processSortOption(sort) { const result = {}; for (const key of Object.keys(sort)) { const sortValue = sort[key]; if (sortValue == null || typeof sortValue !== 'object') { result[key] = sortValue; continue; } const $meta = typeof sortValue === 'object' && sortValue.$meta; if ($meta) { // Astra-db-ts 1.x does not currently support using fields other than $vector and $vectorize // for vector sort and vectorize sort, but that works in tables. astra-mongoose added // support in PR #258 result[key] = $meta; } } return result; } class OperationNotSupportedError extends Error { constructor(message) { super(message); this.name = 'OperationNotSupportedError'; } } exports.OperationNotSupportedError = OperationNotSupportedError; /*! * Compatibility wrapper for Mongoose's `debug` mode * * @param functionName the name of the function being called, like `find` or `updateOne` * @param args arguments passed to the function */ function _logFunctionCall(debug, collectionName, functionName, args) { if (typeof debug === 'function') { debug(collectionName, functionName, ...args); } else if (debug) { console.log(`${collectionName}.${functionName}(${[...args].map(arg => (0, util_1.inspect)(arg, { colors: true })).join(', ')})`); } } //# sourceMappingURL=collection.js.map