@datastax/astra-mongoose
Version:
Astra's NodeJS Mongoose compatibility client
414 lines • 17.1 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.parseUri = exports.Connection = void 0;
const collection_1 = require("./collection");
const db_1 = require("./db");
const operationNotSupportedError_1 = require("../operationNotSupportedError");
const connection_1 = __importDefault(require("mongoose/lib/connection"));
const mongoose_1 = require("mongoose");
const url_1 = require("url");
const assert_1 = __importDefault(require("assert"));
const astra_db_ts_1 = require("@datastax/astra-db-ts");
const astraMongooseError_1 = require("../astraMongooseError");
/**
* Extends Mongoose's Connection class to provide compatibility with Data API. Responsible for maintaining the
* connection to Data API.
*/
class Connection extends connection_1.default {
constructor(base) {
super(base);
this.debugType = 'AstraMongooseConnection';
this.initialConnection = null;
this.client = null;
this.admin = null;
this.db = null;
this.keyspaceName = null;
this.baseUrl = null;
this.baseApiPath = null;
this.models = {};
}
/**
* Helper borrowed from Mongoose to wait for the connection to finish connecting. Because Mongoose
* supports creating a new connection, registering some models, and connecting to the database later.
* This method is private and should not be called by clients.
*
* #### Example:
* const conn = mongoose.createConnection();
* // This may call `createCollection()` internally depending on `autoCreate` option, even though
* // this connection hasn't connected to the database yet.
* conn.model('Test', mongoose.Schema({ name: String }));
* await conn.openUri(uri);
*
* @ignore
*/
async _waitForClient() {
const shouldWaitForClient = (this.readyState === mongoose_1.STATES.connecting || this.readyState === mongoose_1.STATES.disconnected) && this._shouldBufferCommands();
if (shouldWaitForClient) {
await this._waitForConnect();
// Cannot happen, but this helps TypeScript infer the correct return type
assert_1.default.ok(this.db);
assert_1.default.ok(this.admin);
return { db: this.db, admin: this.admin };
}
else if (this.readyState !== mongoose_1.STATES.connected) {
throw new astraMongooseError_1.AstraMongooseError('Connection is not connected', { readyState: this.readyState });
}
// Cannot happen, but this helps TypeScript infer the correct return type
assert_1.default.ok(this.db);
assert_1.default.ok(this.admin);
return { db: this.db, admin: this.admin };
}
/**
* Get a collection by name. Cached in `this.collections`.
* @param name
* @param options
*/
collection(name, options) {
if (!(name in this.collections)) {
this.collections[name] = new collection_1.Collection(name, this, options);
}
return super.collection(name, options);
}
/**
* Create a new collection in the database
* @param name The name of the collection to create
* @param options
*/
async createCollection(name, options) {
const { db } = await this._waitForClient();
return await db.createCollection(name, options);
}
/**
* Get current debug setting, accounting for potential changes to global debug config (`mongoose.set('debug', true | false)`)
*/
get debug() {
return this._debug ?? this.base?.options?.debug;
}
/**
* Create a new table in the database
* @param name
* @param definition
*/
async createTable(name, definition, options) {
const { db } = await this._waitForClient();
return await db.createTable(name, definition, options);
}
/**
* Drop a collection from the database
* @param name
*/
async dropCollection(name, options) {
const { db } = await this._waitForClient();
return await db.dropCollection(name, options);
}
/**
* Drop a table from the database
* @param name The name of the table to drop
*/
async dropTable(name, options) {
const { db } = await this._waitForClient();
return await db.dropTable(name, options);
}
/**
* Create a new keyspace.
*
* @param name The name of the keyspace to create
*/
async createKeyspace(name, options) {
const { admin } = await this._waitForClient();
return await admin.createKeyspace(name, options);
}
/**
* Not implemented.
*
* @ignore
*/
async dropDatabase() {
throw new operationNotSupportedError_1.OperationNotSupportedError('dropDatabase() Not Implemented');
}
async listCollections(options) {
const { db } = await this._waitForClient();
if (options?.nameOnly) {
return await db.listCollections({ ...options, nameOnly: true });
}
return await db.listCollections({ ...options, nameOnly: false });
}
async listTables(options) {
const { db } = await this._waitForClient();
if (options?.nameOnly) {
return await db.listTables({ ...options, nameOnly: true });
}
return await db.listTables({ ...options, nameOnly: false });
}
async listTypes(options) {
const { db } = await this._waitForClient();
if (options?.nameOnly) {
return await db.listTypes({ ...options, nameOnly: true });
}
return await db.listTypes({ ...options, nameOnly: false });
}
/**
* Create a new user-defined type (UDT) with the specified name and fields definition.
* @param name The name of the type to create.
* @param definition The definition of the fields for the type.
* @returns {Promise<TypeDescriptor>} The created type descriptor.
*/
async createType(name, definition) {
const { db } = await this._waitForClient();
return await db.createType(name, definition);
}
/**
* Drop (delete) a user-defined type (UDT) by name.
* @param name The name of the type to drop.
* @returns The result of the dropType command.
*/
async dropType(name, options) {
const { db } = await this._waitForClient();
return await db.dropType(name, options);
}
/**
* Alter a user-defined type (UDT) by renaming or adding fields.
* @param name The name of the type to alter.
* @param update The alterations to be made: renaming or adding fields.
* @returns The result of the alterType command.
*/
async alterType(name, update) {
const { db } = await this._waitForClient();
return await db.alterType(name, update);
}
/**
* Synchronizes the set of user-defined types (UDTs) in the database. It makes existing types in the database
* match the list provided by `types`. New types that are missing are created, and types that exist in the database
* but are not in the input list are dropped. If a type is present in both, we add all the new type's fields to the existing type.
*
* @param types An array of objects each specifying the name and CreateTypeDefinition for a UDT to synchronize.
* @returns An object describing which types were created, updated, or dropped.
* @throws {AstraMongooseError} If an error occurs during type synchronization, with partial progress information in the error.
*/
async syncTypes(types) {
const { db } = await this._waitForClient();
return await db.syncTypes(types);
}
/**
* Run an arbitrary Data API command on the database
* @param command The command to run
*/
async runCommand(command) {
const { db } = await this._waitForClient();
return await db.command(command);
}
/**
* List all keyspaces. Called "listDatabases" for Mongoose compatibility
*/
async listDatabases(options) {
const { admin } = await this._waitForClient();
return { databases: await admin.listKeyspaces(options).then(keyspaces => keyspaces.map(name => ({ name }))) };
}
/**
* Logic for creating a connection to Data API. Mongoose calls `openUri()` internally when the
* user calls `mongoose.create()` or `mongoose.createConnection(uri)`
*
* @param uri the connection string
* @param options
*/
async openUri(uri, options) {
let _fireAndForget = false;
if (options && '_fireAndForget' in options) {
_fireAndForget = !!options._fireAndForget;
delete options._fireAndForget;
}
// Set Mongoose-specific config options. Need to set
// this in order to allow connection-level overrides for
// these options.
this.config = {
autoCreate: options?.autoCreate,
autoIndex: options?.autoIndex,
sanitizeFilter: options?.sanitizeFilter,
bufferCommands: options?.bufferCommands
};
for (const model of Object.values(this.models)) {
void model.init();
}
this.initialConnection = this.createClient(uri, options)
.then(() => this)
.catch(err => {
this.readyState = mongoose_1.STATES.disconnected;
throw err;
});
if (_fireAndForget) {
return this;
}
await this.initialConnection;
return this;
}
/**
* Create an astra-db-ts client and corresponding objects: client, db, admin.
* @param uri the connection string
* @param options
*/
async createClient(uri, options) {
this._connectionString = uri;
this._closeCalled = false;
this.readyState = mongoose_1.STATES.connecting;
const { baseUrl, keyspaceName, applicationToken, baseApiPath } = (0, exports.parseUri)(uri);
const dbOptions = { dataApiPath: baseApiPath };
this._debug = options?.debug;
const isAstra = options?.isAstra ?? true;
const { adminToken, environment } = isAstra
? { adminToken: applicationToken, environment: 'astra' }
: {
adminToken: new astra_db_ts_1.UsernamePasswordTokenProvider(options?.username || throwMissingUsernamePassword(), options?.password || throwMissingUsernamePassword()),
environment: 'dse'
};
const clientOptions = { environment, logging: options?.logging };
if (options?.httpOptions != null) {
clientOptions.httpOptions = options.httpOptions;
}
const client = new astra_db_ts_1.DataAPIClient(adminToken, clientOptions);
const db = options?.isTable
? new db_1.TablesDb(client.db(baseUrl, dbOptions), keyspaceName)
: new db_1.CollectionsDb(client.db(baseUrl, dbOptions), keyspaceName);
const admin = isAstra
? db.astraDb.admin({ adminToken })
: db.astraDb.admin({ adminToken, environment: 'dse' });
const collections = Object.values(this.collections);
for (const collection of collections) {
collection._collection = undefined;
}
this.client = client;
this.db = db;
this.admin = admin;
this.baseUrl = baseUrl;
this.keyspaceName = keyspaceName;
this.baseApiPath = baseApiPath;
this.readyState = mongoose_1.STATES.connected;
this.onOpen();
// Bubble up db-level events from astra-db-ts to the main connection
// Store listener references for later removal
this._dbEventListeners = {
commandStarted: (ev) => this.emit('commandStarted', ev),
commandFailed: (ev) => this.emit('commandFailed', ev),
commandSucceeded: (ev) => this.emit('commandSucceeded', ev),
commandWarnings: (ev) => this.emit('commandWarnings', ev),
};
db.astraDb.on('commandStarted', this._dbEventListeners.commandStarted);
db.astraDb.on('commandFailed', this._dbEventListeners.commandFailed);
db.astraDb.on('commandSucceeded', this._dbEventListeners.commandSucceeded);
db.astraDb.on('commandWarnings', this._dbEventListeners.commandWarnings);
return this;
function throwMissingUsernamePassword() {
throw new astraMongooseError_1.AstraMongooseError('Username and password are required when connecting to non-Astra deployments', { uri, options });
}
}
/**
* Not supported
*
* @param _client
* @ignore
*/
setClient() {
throw new astraMongooseError_1.AstraMongooseError('SetClient not supported');
}
/**
* For consistency with Mongoose's API. `mongoose.createConnection(uri)` returns the connection, **not** a promise,
* so the Mongoose pattern to call `createConnection()` and wait for connection to succeed is
* `await createConnection(uri).asPromise()`
*/
asPromise() {
return this.initialConnection;
}
/**
* Not supported
*
* @ignore
*/
startSession() {
throw new astraMongooseError_1.AstraMongooseError('startSession() Not Implemented');
}
/**
* Mongoose calls `doClose()` to close the connection when the user calls `mongoose.disconnect()` or `conn.close()`.
* Handles closing the astra-db-ts client.
* This method is private and should not be called by clients directly. Mongoose will call this method internally when
* the user calls `mongoose.disconnect()` or `conn.close()`.
*
* @returns Client
* @ignore
*/
async doClose() {
// Remove db-level event listeners if present
if (this.db && this._dbEventListeners) {
const dbEmitter = this.db.astraDb;
dbEmitter.off('commandStarted', this._dbEventListeners.commandStarted);
dbEmitter.off('commandFailed', this._dbEventListeners.commandFailed);
dbEmitter.off('commandSucceeded', this._dbEventListeners.commandSucceeded);
dbEmitter.off('commandWarnings', this._dbEventListeners.commandWarnings);
this._dbEventListeners = undefined;
}
if (this.client != null) {
await this.client.close();
}
return this;
}
// @ts-expect-error Mongoose connection is typed as any here
on(event, listener) {
return super.on(event, listener);
}
// @ts-expect-error Mongoose connection is typed as any here
once(event, listener) {
return super.once(event, listener);
}
// @ts-expect-error Mongoose connection is typed as any here
emit(event, eventData) {
return super.emit(event, eventData);
}
}
exports.Connection = Connection;
// Parse a connection URI in the format of: https://${baseUrl}/${baseAPIPath}/${keyspace}?applicationToken=${applicationToken}
const parseUri = (uri) => {
const parsedUrl = new url_1.URL(uri);
const baseUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
// Remove trailing slash from pathname before use
// /v1/testks1/ => /v1/testks1
// /apis/v1/testks1/ => /apis/v1/testks1
// /testks1/ => /testks1
// / => '' (empty string)
const pathname = parsedUrl.pathname.replace(/\/$/, '');
const keyspaceName = pathname.substring(pathname.lastIndexOf('/') + 1);
// Remove the last part of the api path (which is assumed as the keyspace name). For example:
// /v1/testks1 => v1
// /apis/v1/testks1 => apis/v1
// /testks1 => '' (empty string)
const baseApiPath = pathname.substring(1, pathname.lastIndexOf('/'));
const applicationToken = parsedUrl.searchParams.get('applicationToken') ?? undefined;
// Check for duplicate application tokens
if (parsedUrl.searchParams.getAll('applicationToken').length > 1) {
throw new Error('Invalid URI: multiple application tokens');
}
if (keyspaceName.length === 0) {
throw new Error('Invalid URI: keyspace is required');
}
return {
baseUrl,
baseApiPath,
keyspaceName,
applicationToken
};
};
exports.parseUri = parseUri;
//# sourceMappingURL=connection.js.map