@datastax/astra-mongoose
Version:
Astra's NodeJS Mongoose compatibility client
452 lines • 20.8 kB
JavaScript
"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