diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d29829e..324879a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Install Deno uses: denolib/setup-deno@master with: - deno-version: 1.x.x + deno-version: 2.x.x - name: Log versions run: | diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml index 6e91d51..0964a7e 100644 --- a/.github/workflows/jsr.yml +++ b/.github/workflows/jsr.yml @@ -33,4 +33,4 @@ jobs: - name: Publish to JSR run: | - deno publish \ No newline at end of file + deno publish diff --git a/deps.ts b/deps.ts index a70a811..68b1346 100644 --- a/deps.ts +++ b/deps.ts @@ -16,6 +16,6 @@ export { Timestamp, UUID, } from "jsr:@lucsoft/web-bson@^0.3.1"; -export { crypto as stdCrypto } from "jsr:@std/crypto@^0.224.0/crypto"; -export { decodeBase64, encodeBase64 } from "jsr:@std/encoding@^0.224.0/base64"; -export { encodeHex } from "jsr:@std/encoding@^0.224.0/hex"; +export { crypto as stdCrypto } from "jsr:@std/crypto@^1.0.3/crypto"; +export { decodeBase64, encodeBase64 } from "jsr:@std/encoding@^1.0.5/base64"; +export { encodeHex } from "jsr:@std/encoding@^1.0.5/hex"; diff --git a/src/client.ts b/src/client.ts index 7090500..bdf35fc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -41,9 +41,7 @@ export class MongoClient { * * @param options Connection options or a MongoDB URI */ - async connect( - options: ConnectOptions | string, - ): Promise { + async connect(options: ConnectOptions | string): Promise { try { const parsedOptions = typeof options === "string" ? await parse(options) @@ -59,8 +57,10 @@ export class MongoClient { this.#buildInfo = await this.runCommand(this.#defaultDbName, { buildInfo: 1, }); - } catch (e) { - throw new MongoDriverError(`Connection failed: ${e.message || e}`); + } catch (e: unknown) { + throw new MongoDriverError( + `Connection failed: ${e instanceof Error ? e.message : "unknown"}`, + ); } return this.database((options as ConnectOptions).db); } @@ -71,12 +71,14 @@ export class MongoClient { * @param options Options to pass to the `listDatabases` command * @returns A list of databases including their name, size on disk, and whether they are empty */ - async listDatabases(options: { - filter?: Document; - nameOnly?: boolean; - authorizedCollections?: boolean; - comment?: Document; - } = {}): Promise { + async listDatabases( + options: { + filter?: Document; + nameOnly?: boolean; + authorizedCollections?: boolean; + comment?: Document; + } = {}, + ): Promise { const { databases } = await this.getCluster().protocol.commandSingle( "admin", { diff --git a/src/error.ts b/src/error.ts index e0732a4..4e9ae22 100644 --- a/src/error.ts +++ b/src/error.ts @@ -73,7 +73,7 @@ export class MongoRuntimeError extends MongoDriverError { super(message); } - get name(): string { + override get name(): string { return "MongoRuntimeError"; } } diff --git a/tests/cases/03_crud.ts b/tests/cases/03_crud.ts index 7beb539..3979c6d 100644 --- a/tests/cases/03_crud.ts +++ b/tests/cases/03_crud.ts @@ -1,3 +1,4 @@ +import { assertInstanceOf } from "jsr:@std/assert@^0.220.1/assert_instance_of"; import type { Database, MongoClient, ObjectId } from "../../mod.ts"; import { MongoInvalidArgumentError, @@ -259,10 +260,13 @@ describe("crud operations", () => { "mongo_test_users", ); await users.insertOne({ username: "counter", counter: 5 }); - const updated = await users.findAndModify({ username: "counter" }, { - update: { $inc: { counter: 1 } }, - new: true, - }); + const updated = await users.findAndModify( + { username: "counter" }, + { + update: { $inc: { counter: 1 } }, + new: true, + }, + ); assert(updated !== null); assertEquals(updated.counter, 6); @@ -274,9 +278,12 @@ describe("crud operations", () => { "mongo_test_users", ); await users.insertOne({ username: "delete", counter: 10 }); - const updated = await users.findAndModify({ username: "delete" }, { - remove: true, - }); + const updated = await users.findAndModify( + { username: "delete" }, + { + remove: true, + }, + ); assert(updated !== null); assertEquals(updated.counter, 10); @@ -330,9 +337,12 @@ describe("crud operations", () => { }, friends: ["Alice", "Bob"], }); - const result = await users.updateOne({}, { - $push: { friends: { $each: ["Carol"] } }, - }); + const result = await users.updateOne( + {}, + { + $push: { friends: { $each: ["Carol"] } }, + }, + ); assertEquals(result, { matchedCount: 1, modifiedCount: 1, @@ -350,9 +360,12 @@ describe("crud operations", () => { }, friends: ["Alice", "Bob"], }); - const result = await users.updateOne({}, { - $pull: { friends: "Bob" }, - }); + const result = await users.updateOne( + {}, + { + $pull: { friends: "Bob" }, + }, + ); assertEquals(result, { matchedCount: 1, modifiedCount: 1, @@ -370,9 +383,12 @@ describe("crud operations", () => { }, friends: ["Alice", "Bob"], }); - const result = await users.updateOne({}, { - $push: { "likes.hobbies.indoor": "board games" }, - }); + const result = await users.updateOne( + {}, + { + $push: { "likes.hobbies.indoor": "board games" }, + }, + ); assertEquals(result, { matchedCount: 1, modifiedCount: 1, @@ -390,9 +406,12 @@ describe("crud operations", () => { }, friends: ["Alice", "Bob"], }); - const result = await users.updateOne({}, { - $pullAll: { "likes.hobbies.indoor": ["board games", "cooking"] }, - }); + const result = await users.updateOne( + {}, + { + $pullAll: { "likes.hobbies.indoor": ["board games", "cooking"] }, + }, + ); assertEquals(result, { matchedCount: 1, modifiedCount: 1, @@ -410,9 +429,12 @@ describe("crud operations", () => { }, friends: ["Alice", "Bob"], }); - const result = await users.updateOne({}, { - $pull: { "likes.hobbies.indoor": "board games" }, - }); + const result = await users.updateOne( + {}, + { + $pull: { "likes.hobbies.indoor": "board games" }, + }, + ); assertEquals(result, { matchedCount: 1, modifiedCount: 1, @@ -421,7 +443,8 @@ describe("crud operations", () => { }); }); - it("testUpdateOne Error", async () => { // TODO: move tesr errors to a new file + it("testUpdateOne Error", async () => { + // TODO: move tesr errors to a new file const users = database.collection("mongo_test_users"); await users.insertOne({ username: "user1", @@ -457,9 +480,12 @@ describe("crud operations", () => { username: "user1", password: "pass1", }); - const result = await users.replaceOne({ username: "user1" }, { - username: "user2", - }); + const result = await users.replaceOne( + { username: "user1" }, + { + username: "user2", + }, + ); assertEquals(result, { matchedCount: 1, @@ -813,25 +839,39 @@ describe("crud operations", () => { uid: i, }); } - const users1 = await users.find({ - uid: 0, - }, { maxTimeMS: 100 }).toArray(); + const users1 = await users + .find( + { + uid: 0, + }, + { maxTimeMS: 100 }, + ) + .toArray(); assertEquals(users1.length, 1); - const user1 = await users.findOne({ - uid: 0, - }, { maxTimeMS: 100 }); + const user1 = await users.findOne( + { + uid: 0, + }, + { maxTimeMS: 100 }, + ); assertEquals(user1!.uid, 0); try { - await users.find({ - uid: 0, - $where: "sleep(10) || true", - }, { maxTimeMS: 1 }).toArray(); + await users + .find( + { + uid: 0, + $where: "sleep(10) || true", + }, + { maxTimeMS: 1 }, + ) + .toArray(); assert(false); } catch (e) { + assertInstanceOf(e, MongoServerError); assertEquals(e.ok, 0); assertEquals(e.codeName, "MaxTimeMSExpired"); assertEquals(e.errmsg, "operation exceeded time limit"); @@ -839,12 +879,16 @@ describe("crud operations", () => { if (supportsMaxTimeMSInFindOne) { try { - await users.findOne({ - uid: 0, - $where: "sleep(10) || true", - }, { maxTimeMS: 1 }); + await users.findOne( + { + uid: 0, + $where: "sleep(10) || true", + }, + { maxTimeMS: 1 }, + ); assert(false); } catch (e) { + assertInstanceOf(e, MongoServerError); assertEquals(e.ok, 0); assertEquals(e.codeName, "MaxTimeMSExpired"); assertEquals(e.errmsg, "operation exceeded time limit"); @@ -854,128 +898,116 @@ describe("crud operations", () => { await database.collection("mongo_test_users").drop(); }); - it( - "createCollection should create a collection without options", - async () => { - const createdCollection = await database - .createCollection<{ _id: string; name: string }>( - testCollectionName, - ); + it("createCollection should create a collection without options", async () => { + const createdCollection = await database.createCollection<{ + _id: string; + name: string; + }>(testCollectionName); - assert(createdCollection); - - await database.collection(testCollectionName).drop(); - }, - ); - - it( - "createCollection should create a collection with options", - async () => { - interface IStudents { - _id: string; - name: string; - year: number; - major: string; - gpa?: number; - address: { - city: string; - street: string; - }; - } + assert(createdCollection); - // Example from https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/#mongodb-query-op.-jsonSchema - const options: CreateCollectionOptions = { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["name", "year", "major", "address"], - properties: { - name: { - bsonType: "string", - description: "must be a string and is required", - }, - year: { - bsonType: "int", - minimum: 2017, - maximum: 3017, - description: - "must be an integer in [ 2017, 3017 ] and is required", - }, - major: { - enum: ["Math", "English", "Computer Science", "History", null], - description: - "can only be one of the enum values and is required", - }, - gpa: { - bsonType: ["double"], - description: "must be a double if the field exists", - }, - address: { - bsonType: "object", - required: ["city"], - properties: { - street: { - bsonType: "string", - description: "must be a string if the field exists", - }, - city: { - bsonType: "string", - "description": "must be a string and is required", - }, + await database.collection(testCollectionName).drop(); + }); + + it("createCollection should create a collection with options", async () => { + interface IStudents { + _id: string; + name: string; + year: number; + major: string; + gpa?: number; + address: { + city: string; + street: string; + }; + } + + // Example from https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/#mongodb-query-op.-jsonSchema + const options: CreateCollectionOptions = { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["name", "year", "major", "address"], + properties: { + name: { + bsonType: "string", + description: "must be a string and is required", + }, + year: { + bsonType: "int", + minimum: 2017, + maximum: 3017, + description: + "must be an integer in [ 2017, 3017 ] and is required", + }, + major: { + enum: ["Math", "English", "Computer Science", "History", null], + description: "can only be one of the enum values and is required", + }, + gpa: { + bsonType: ["double"], + description: "must be a double if the field exists", + }, + address: { + bsonType: "object", + required: ["city"], + properties: { + street: { + bsonType: "string", + description: "must be a string if the field exists", + }, + city: { + bsonType: "string", + description: "must be a string and is required", }, }, }, }, }, - }; + }, + }; - const createdCollection = await database - .createCollection( - testCollectionName, - options, - ); + const createdCollection = await database.createCollection( + testCollectionName, + options, + ); - assert(createdCollection); - - // sanity test to check whether the speicified validator from options works - // error with message: "Document failed validation" - await assertRejects( - () => - createdCollection.insertOne({ - name: "Alice", - year: 2019, - major: "History", - gpa: 3, - address: { - city: "NYC", - street: "33rd Street", - }, - }), - ); - - // TODO: refactor to clean up the test collection properly. - // It should clean up the collection when above insertion succeeds in any case, which is unwanted result. - // Refactor when test utility is more provided. - await database.collection(testCollectionName).drop(); - }, - ); - - it( - "createCollection should throw an error with invalid options", - async () => { - const invalidOptions: CreateCollectionOptions = { - capped: true, - }; + assert(createdCollection); - await assertRejects( - () => - database.createCollection<{ _id: string; name: string }>( - testCollectionName, - invalidOptions, - ), - // error with the message "the 'size' field is required when 'capped' is true" - MongoServerError, - ); - }, - ); + // sanity test to check whether the speicified validator from options works + // error with message: "Document failed validation" + await assertRejects(() => + createdCollection.insertOne({ + name: "Alice", + year: 2019, + major: "History", + gpa: 3, + address: { + city: "NYC", + street: "33rd Street", + }, + }) + ); + + // TODO: refactor to clean up the test collection properly. + // It should clean up the collection when above insertion succeeds in any case, which is unwanted result. + // Refactor when test utility is more provided. + await database.collection(testCollectionName).drop(); + }); + + it("createCollection should throw an error with invalid options", async () => { + const invalidOptions: CreateCollectionOptions = { + capped: true, + }; + + await assertRejects( + () => + database.createCollection<{ _id: string; name: string }>( + testCollectionName, + invalidOptions, + ), + // error with the message "the 'size' field is required when 'capped' is true" + MongoServerError, + ); + }); }); diff --git a/tests/cases/05_srv.ts b/tests/cases/05_srv.ts index 93f15f7..09f1421 100644 --- a/tests/cases/05_srv.ts +++ b/tests/cases/05_srv.ts @@ -2,7 +2,7 @@ import { Srv } from "../../src/utils/srv.ts"; import { assertEquals, assertRejects, describe, it } from "../deps.ts"; function mockResolver( - srvRecords: Partial[] = [], + srvRecords: Partial[] = [], txtRecords: string[][] = [], ) { return { @@ -43,8 +43,9 @@ describe("SRV", () => { fn() { assertRejects( () => - new Srv(mockResolver([{ target: "mongohost1.mongodomain.com" }])) - .resolve("mongohost.mongodomain.com"), + new Srv( + mockResolver([{ target: "mongohost1.mongodomain.com" }]), + ).resolve("mongohost.mongodomain.com"), Error, "Expected exactly one TXT record, received 0 for url mongohost.mongodomain.com", ); @@ -62,8 +63,7 @@ describe("SRV", () => { [{ target: "mongohost1.mongodomain.com" }], [["replicaSet=rs-0"], ["authSource=admin"]], ), - ) - .resolve("mongohost.mongodomain.com"), + ).resolve("mongohost.mongodomain.com"), Error, "Expected exactly one TXT record, received 2 for url mongohost.mongodomain.com", ); @@ -80,8 +80,7 @@ describe("SRV", () => { [{ target: "mongohost1.mongodomain.com" }], [["replicaSet=rs-0&authSource=admin&ssl=true"]], ), - ) - .resolve("mongohost.mongodomain.com"), + ).resolve("mongohost.mongodomain.com"), Error, "Illegal uri options: ssl=true", ); @@ -92,23 +91,26 @@ describe("SRV", () => { name: "SRV: it correctly parses seedlist and options for valid records", async fn() { const result = await new Srv( - mockResolver([ - { - target: "mongohost1.mongodomain.com", - port: 27015, - }, - { - target: "mongohost2.mongodomain.com", - port: 27017, - }, - ], [["replicaSet=rs-0&authSource=admin"]]), + mockResolver( + [ + { + target: "mongohost1.mongodomain.com", + port: 27015, + }, + { + target: "mongohost2.mongodomain.com", + port: 27017, + }, + ], + [["replicaSet=rs-0&authSource=admin"]], + ), ).resolve("mongohost.mongodomain.com"); assertEquals(result.servers.length, 2); const server1 = result.servers.find( (server) => server.host === "mongohost1.mongodomain.com", ); - const server2 = result.servers.find((server) => - server.host === "mongohost2.mongodomain.com" + const server2 = result.servers.find( + (server) => server.host === "mongohost2.mongodomain.com", ); assertEquals(server1!.port, 27015); assertEquals(server2!.port, 27017); @@ -123,23 +125,26 @@ describe("SRV", () => { "SRV: it correctly parses seedlist and options for options split in two strings", async fn() { const result = await new Srv( - mockResolver([ - { - target: "mongohost1.mongodomain.com", - port: 27015, - }, - { - target: "mongohost2.mongodomain.com", - port: 27017, - }, - ], [["replicaS", "et=rs-0&authSource=admin"]]), + mockResolver( + [ + { + target: "mongohost1.mongodomain.com", + port: 27015, + }, + { + target: "mongohost2.mongodomain.com", + port: 27017, + }, + ], + [["replicaS", "et=rs-0&authSource=admin"]], + ), ).resolve("mongohost.mongodomain.com"); assertEquals(result.servers.length, 2); const server1 = result.servers.find( (server) => server.host === "mongohost1.mongodomain.com", ); - const server2 = result.servers.find((server) => - server.host === "mongohost2.mongodomain.com" + const server2 = result.servers.find( + (server) => server.host === "mongohost2.mongodomain.com", ); assertEquals(server1!.port, 27015); assertEquals(server2!.port, 27017);