diff --git a/api-compatibility.versions b/api-compatibility.versions index 16b885a..cb01d64 100644 --- a/api-compatibility.versions +++ b/api-compatibility.versions @@ -1,2 +1,2 @@ -stargate_version=v2.1.0-BETA-14 -data_api_version=v1.0.16 +stargate_version=v2.1.0-BETA-18 +data_api_version=v1.0.17 diff --git a/src/collections/collection.ts b/src/collections/collection.ts index 6aca27d..593356c 100644 --- a/src/collections/collection.ts +++ b/src/collections/collection.ts @@ -387,6 +387,58 @@ export class Collection { }); } + /** + * + * @param column + * @param indexName + * @returns Promise + */ + async createIndex(column: string, indexName: string) { + if (column == null) { + throw new TypeError(`Must specify a column when calling createIndex, got ${column}`); + } + if (indexName == null) { + throw new TypeError(`Must specify an indexName when calling createIndex, got ${indexName}`); + } + return executeOperation(async () => { + const command = { + addIndex: { + column, + indexName + } + }; + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); + }); + } + + /** + * + * @param column + * @param indexName + * @returns Promise + */ + async dropIndex(indexName: string) { + if (indexName == null) { + throw new TypeError(`Must specify an indexName when calling dropIndex, got ${indexName}`); + } + return executeOperation(async () => { + const command = { + dropIndex: { + indexName + } + }; + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); + }); + } + async runCommand(command: Record) { return executeOperation(async () => { return await this.httpClient.executeCommandWithUrl( diff --git a/src/collections/db.ts b/src/collections/db.ts index 3e0d2d1..780e4ec 100644 --- a/src/collections/db.ts +++ b/src/collections/db.ts @@ -16,6 +16,7 @@ import { HTTPClient } from '@/src/client'; import { CreateCollectionOptions, createCollectionOptionsKeys, + CreateTableDefinition, ListCollectionOptions, listCollectionOptionsKeys } from './options'; @@ -84,6 +85,67 @@ export class Db { }); } + /** + * + * @param name + * @param options + * @returns Promise + */ + async createTable(name: string, definition: CreateTableDefinition) { + if (name == null) { + throw new TypeError(`Must specify a name when calling createTable, got ${name}`); + } + return executeOperation(async () => { + const command = { + createTable: { + name, + definition + } + }; + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); + }); + } + + async listTables() { + return executeOperation(async () => { + const command = { + listTables: {} + }; + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); + }); + } + + /** + * + * @param name + * @returns Promise + */ + async dropTable(name: string) { + if (name == null) { + throw new TypeError(`Must specify a name when calling dropTable, got ${name}`); + } + return executeOperation(async () => { + const command = { + dropTable: { + name + } + }; + return await this.httpClient.executeCommandWithUrl( + this.httpBasePath, + command, + null + ); + }); + } + /** * * @param collectionName diff --git a/src/collections/options.ts b/src/collections/options.ts index 9ce69d2..910d75b 100644 --- a/src/collections/options.ts +++ b/src/collections/options.ts @@ -155,6 +155,13 @@ export const createCollectionOptionsKeys: Set = new Set( Object.keys(new _CreateCollectionOptions) ); +type TableDataTypes = 'text' | 'int' | 'boolean' | 'bigint' | 'decimal' | 'double' | 'float' | 'smallint' | 'tinyint' | 'varint' | 'ascii'; + +export interface CreateTableDefinition { + columns: Record, + primaryKey: string +} + class _ListCollectionOptions { explain?: boolean = undefined; } diff --git a/src/driver/collection.ts b/src/driver/collection.ts index 2c0486c..b72a21d 100644 --- a/src/driver/collection.ts +++ b/src/driver/collection.ts @@ -273,6 +273,25 @@ export class Collection extends MongooseCollection { return this.collection.updateMany(filter, update, options); } + /** + * + * @param fieldOrSpec + * @param options + */ + createIndex(fieldOrSpec: Record | string, options: { name: string, [other: string]: any }) { + if (fieldOrSpec != null && typeof fieldOrSpec === 'object' && Object.keys(fieldOrSpec).length > 1) { + throw new TypeError('Can only index one key'); + } + const field: string = typeof fieldOrSpec === 'string' ? fieldOrSpec : Object.keys(fieldOrSpec)[0]; + if (typeof fieldOrSpec !== 'string') { + throw new TypeError('Invalid index specification'); + } + if (typeof options?.name !== 'string') { + throw new TypeError('Must provide `name` option to `createIndex()`'); + } + return this.collection.createIndex(field, options.name); + } + /** * Get the estimated number of documents in a collection based on collection metadata */ @@ -331,15 +350,6 @@ export class Collection extends MongooseCollection { throw new OperationNotSupportedError('listIndexes() Not Implemented'); } - /** - * Create index not supported. - * @param fieldOrSpec - * @param options - */ - createIndex(fieldOrSpec: any, options?: any) { - throw new OperationNotSupportedError('createIndex() Not Implemented'); - } - /** * Drop indexes not supported. */ diff --git a/src/driver/connection.ts b/src/driver/connection.ts index e19802b..c71b46e 100644 --- a/src/driver/connection.ts +++ b/src/driver/connection.ts @@ -17,7 +17,7 @@ import { Collection } from './collection'; import { default as MongooseConnection } from 'mongoose/lib/connection'; import { STATES } from 'mongoose'; import { executeOperation } from '../collections/utils'; -import { CreateCollectionOptions } from '../collections/options'; +import { CreateCollectionOptions, CreateTableDefinition } from '../collections/options'; export class Connection extends MongooseConnection { debugType = 'StargateMongooseConnection'; @@ -58,6 +58,31 @@ export class Connection extends MongooseConnection { }); } + async createTable(name: string, definition: CreateTableDefinition) { + return executeOperation(async () => { + await this._waitForClient(); + const db = this.client.db(); + return db.createTable(name, definition); + }); + } + + async listTables(): Promise { + return executeOperation(async () => { + await this._waitForClient(); + const db = this.client.db(); + const res: { status: { tables: string[] } } = await db.listTables(); + return res.status.tables; + }); + } + + async createIndex(column: string, indexName: string) { + return executeOperation(async () => { + await this._waitForClient(); + const db = this.client.db(); + return db.createIndex(column, indexName); + }); + } + async dropCollection(name: string) { return executeOperation(async () => { await this._waitForClient(); @@ -66,6 +91,22 @@ export class Connection extends MongooseConnection { }); } + async dropTable(name: string) { + return executeOperation(async () => { + await this._waitForClient(); + const db = this.client.db(); + return db.dropTable(name); + }); + } + + async dropIndex(name: string) { + return executeOperation(async () => { + await this._waitForClient(); + const db = this.client.db(); + return db.dropTable(name); + }); + } + async dropDatabase() { return executeOperation(async () => { await this._waitForClient(); diff --git a/tests/collections/collection.test.ts b/tests/collections/collection.test.ts index fcb1040..bb50d32 100644 --- a/tests/collections/collection.test.ts +++ b/tests/collections/collection.test.ts @@ -235,7 +235,7 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti error = e; } assert.ok(error); - assert.strictEqual(error.errors[0].message, 'Failed to insert document with _id \'docml10\': Document already exists with the given _id'); + assert.strictEqual(error.errors[0].message, 'Failed to insert document with _id docml10: Document already exists with the given _id'); assert.strictEqual(error.errors[0].errorCode, 'DOCUMENT_ALREADY_EXISTS'); assert.strictEqual(error.status.insertedIds.length, 10); docList.slice(0, 10).forEach((doc, index) => { @@ -315,7 +315,7 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti error = e; } assert.ok(error); - assert.strictEqual(error.errors[0].message, 'Failed to insert document with _id \'docml10\': Document already exists with the given _id'); + assert.strictEqual(error.errors[0].message, 'Failed to insert document with _id docml10: Document already exists with the given _id'); assert.strictEqual(error.errors[0].errorCode, 'DOCUMENT_ALREADY_EXISTS'); assert.strictEqual(error.status.insertedIds.length, 19); //check if response insertedIds contains all the docs except the one that failed @@ -2808,4 +2808,45 @@ describe(`StargateMongoose - ${testClientName} Connection - collections.collecti assert.strictEqual(count, 0); }); }); + + describe('tables and indexes', () => { + const tableName = 'test_table'; + + before(function () { + if (isAstra) { + return this.skip(); + } + if (!process.env.TABLES_ENABLED) { + return this.skip(); + } + }); + + before(async () => { + await db.createTable(tableName, { + primaryKey: 'id', + columns: { id: { type: 'text' }, age: { type: 'int' } } + }); + }); + + after(async () => { + if (isAstra || !process.env.TABLES_ENABLED) { + return; + } + await db.dropTable(tableName); + }); + + it('can create and drop index', async function() { + await db.collection(tableName).createIndex('age', 'ageindex'); + + await db.collection(tableName).insertOne({ id: 'test', age: 42 }); + const doc = await db.collection(tableName).findOne({ age: 42 }); + assert.strictEqual(doc!.id, 'test'); + + await db.collection(tableName).dropIndex('ageindex'); + await assert.rejects( + () => db.collection(tableName).findOne({ age: 42 }), + /NO_INDEX_ERROR/ + ); + }); + }); }); diff --git a/tests/collections/db.test.ts b/tests/collections/db.test.ts index 1814a84..5e68120 100644 --- a/tests/collections/db.test.ts +++ b/tests/collections/db.test.ts @@ -309,4 +309,37 @@ describe('StargateMongoose - collections.Db', async () => { } }); }); + + describe('tables', function () { + before(function () { + if (isAstra) { + return this.skip(); + } + if (!process.env.TABLES_ENABLED) { + return this.skip(); + } + }); + + it('should create and drop a table', async () => { + const name = TEST_COLLECTION_NAME + '_table'; + const db = new Db(httpClient, parseUri(dbUri).keyspaceName); + await db.createTable( + name, + { + primaryKey: 'id', + columns: { id: { type: 'text' }, age: { type: 'int' } } + } + ); + + await db.collection(name).insertOne({ id: 'test', age: 42 }); + const doc = await db.collection(name).findOne({ id: 'test' }); + assert.equal(doc!.age, 42); + + await db.dropTable(name); + await assert.rejects( + () => db.collection(name).insertOne({ id: 'test', age: 42 }), + /COLLECTION_NOT_EXIST/ + ); + }); + }); }); \ No newline at end of file diff --git a/tests/driver/api.test.ts b/tests/driver/api.test.ts index b10ec21..054005e 100644 --- a/tests/driver/api.test.ts +++ b/tests/driver/api.test.ts @@ -706,6 +706,56 @@ describe('Mongoose Model API level tests', async () => { // @ts-ignore assert.ok(databases.includes(mongooseInstance.connection.db.name)); }); + it('API ops tests createTable() listTables() dropTable() createIndex() dropIndex()', async function () { + if (testClient.isAstra) { + return this.skip(); + } + if (!process.env.TABLES_ENABLED) { + return this.skip(); + } + + const mongoose = new mongooseInstance.Mongoose(); + mongoose.setDriver(StargateMongooseDriver); + mongoose.set('autoCreate', false); + mongoose.set('autoIndex', false); + const options = { + username: process.env.STARGATE_USERNAME, + password: process.env.STARGATE_PASSWORD, + featureFlags: ['Feature-Flag-tables'] + }; + await mongoose.connect(testClient!.uri, options as mongoose.ConnectOptions); + + const name = 'test_table'; + await mongoose!.connection.dropTable(name); + await mongoose!.connection.createTable( + name, + { + primaryKey: 'id', + columns: { id: { type: 'text' }, age: { type: 'int' } } + } + ); + + const tables = await mongoose!.connection.listTables(); + assert.ok(tables.includes(name), tables.join(', ')); + + await mongoose!.connection.db.collection(name).createIndex('age', 'ageindex'); + + await mongoose!.connection.db.collection(name).insertOne({ id: 'test', age: 42 }); + let doc = await mongoose!.connection.db.collection(name).findOne({ id: 'test' }); + assert.equal(doc!.age, 42); + doc = await mongoose!.connection.db.collection(name).findOne({ age: 42 }); + assert.equal(doc!.id, 'test'); + + await mongoose!.connection.db.collection(name).dropIndex('ageindex'); + + await mongoose!.connection.dropTable(name); + await assert.rejects( + () => mongoose!.connection.db.collection(name).insertOne({ id: 'test', age: 42 }), + /COLLECTION_NOT_EXIST/ + ); + + await mongoose.disconnect(); + }); it('API ops tests connection.runCommand()', async () => { const res = await mongooseInstance!.connection.runCommand({ findCollections: {} }); assert.ok(res.status.collections.includes('carts'));