diff --git a/client.ts b/client.ts new file mode 100644 index 0000000..79acef4 --- /dev/null +++ b/client.ts @@ -0,0 +1,216 @@ +import { Bson } from "./deps.ts"; + +export interface MongoClientConstructorOptions { + appId: string; + dataSource: string; + apiKey: string; + endpoint?: string; + fetch?: typeof fetch; +} + +export class MongoClient { + appId: string; + dataSource: string; + apiKey: string; + endpoint = "https://data.mongodb-api.com"; + fetch = fetch; + + constructor( + { appId, dataSource, apiKey, endpoint, fetch }: + MongoClientConstructorOptions, + ) { + this.appId = appId; + this.dataSource = dataSource; + this.apiKey = apiKey; + if (endpoint) { + this.endpoint = endpoint; + } + + if (fetch) { + this.fetch = fetch; + } + } + + database(name: string) { + return new Database(name, this); + } +} + +class Database { + name: string; + client: MongoClient; + + constructor(name: string, client: MongoClient) { + this.name = name; + this.client = client; + } + + collection(name: string) { + return new Collection(name, this); + } +} + +class Collection { + name: string; + database: Database; + client: MongoClient; + + constructor(name: string, database: Database) { + this.name = name; + this.database = database; + this.client = database.client; + } + + insertOne(doc: T): Promise<{ insertedId: string }> { + return this.callApi("insertOne", { document: doc }); + } + + insertMany(docs: T[]): Promise<{ insertedIds: string[] }> { + return this.callApi("insertMany", { documents: docs }); + } + + async findOne( + filter: Bson.Document, + { projection }: { projection?: Bson.Document } = {}, + ): Promise { + const result = await this.callApi("findOne", { + filter, + projection, + }); + return result.document; + } + + async find( + filter?: Bson.Document, + { projection, sort, limit, skip }: { + projection?: Bson.Document; + sort?: Bson.Document; + limit?: number; + skip?: number; + } = {}, + ): Promise { + const result = await this.callApi("find", { + filter, + projection, + sort, + limit, + skip, + }); + return result.documents; + } + + updateOne( + filter: Bson.Document, + update: Bson.Document, + { upsert }: { upsert?: boolean }, + ): Promise< + { matchedCount: number; modifiedCount: number; upsertedId?: string } + > { + return this.callApi("updateOne", { + filter, + update, + upsert, + }); + } + + updateMany( + filter: Bson.Document, + update: Bson.Document, + { upsert }: { upsert?: boolean }, + ): Promise< + { matchedCount: number; modifiedCount: number; upsertedId?: string } + > { + return this.callApi("updateMany", { + filter, + update, + upsert, + }); + } + + replaceOne( + filter: Bson.Document, + replacement: Bson.Document, + { upsert }: { upsert?: boolean }, + ): Promise< + { matchedCount: number; modifiedCount: number; upsertedId?: string } + > { + return this.callApi("replaceOne", { + filter, + replacement, + upsert, + }); + } + + deleteOne(filter: Bson.Document): Promise<{ deletedCount: number }> { + return this.callApi("deleteOne", { filter }); + } + + deleteMany(filter: Bson.Document): Promise<{ deletedCount: number }> { + return this.callApi("deleteMany", { filter }); + } + + async aggregate(pipeline: Bson.Document[]): Promise { + const result = await this.callApi("aggregate", { pipeline }); + return result.documents; + } + + async countDocuments( + filter?: Bson.Document, + options?: { limit?: number; skip?: number }, + ): Promise { + const pipeline: Bson.Document[] = []; + if (filter) { + pipeline.push({ $match: filter }); + } + + if (typeof options?.skip === "number") { + pipeline.push({ $skip: options.limit }); + } + + if (typeof options?.limit === "number") { + pipeline.push({ $limit: options.limit }); + } + + pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } }); + + const [result] = await this.aggregate<{ n: number }>(pipeline); + if (result) return result.n; + return 0; + } + + async estimatedDocumentCount(): Promise { + const pipeline = [ + { $collStats: { count: {} } }, + { $group: { _id: 1, n: { $sum: "$count" } } }, + ]; + + const [result] = await this.aggregate<{ n: number }>(pipeline); + if (result) return result.n; + return 0; + } + + async callApi(method: string, extra: Bson.Document) { + const { endpoint, appId, apiKey, dataSource } = this.client; + const url = `${endpoint}/app/${appId}/endpoint/data/beta/action/${method}`; + + const response = await this.client.fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "api-key": apiKey, + }, + body: Bson.EJSON.stringify({ + collection: this.name, + database: this.database.name, + dataSource: dataSource, + ...extra, + }), + }); + + if (response.ok) { + return response.json(); + } + + throw new Error(`${response.statusText}: ${await response.text()}`); + } +} diff --git a/deps.ts b/deps.ts index 987521b..aac36e6 100644 --- a/deps.ts +++ b/deps.ts @@ -1 +1 @@ -export * as Bson from "https://esm.sh/bson@4.6.2"; +export * as Bson from "https://deno.land/x/web_bson@v0.2.0/mod.ts"; diff --git a/mod.ts b/mod.ts index d09e96a..5e50366 100644 --- a/mod.ts +++ b/mod.ts @@ -1,217 +1,3 @@ -import { Bson } from "./deps.ts"; -export { Bson }; - -export interface MongoClientConstructorOptions { - appId: string; - dataSource: string; - apiKey: string; - endpoint?: string; - fetch?: typeof fetch; -} - -export class MongoClient { - appId: string; - dataSource: string; - apiKey: string; - endpoint = "https://data.mongodb-api.com"; - fetch = fetch; - - constructor( - { appId, dataSource, apiKey, endpoint, fetch }: - MongoClientConstructorOptions, - ) { - this.appId = appId; - this.dataSource = dataSource; - this.apiKey = apiKey; - if (endpoint) { - this.endpoint = endpoint; - } - - if (fetch) { - this.fetch = fetch; - } - } - - database(name: string) { - return new Database(name, this); - } -} - -class Database { - name: string; - client: MongoClient; - - constructor(name: string, client: MongoClient) { - this.name = name; - this.client = client; - } - - collection(name: string) { - return new Collection(name, this); - } -} - -class Collection { - name: string; - database: Database; - client: MongoClient; - - constructor(name: string, database: Database) { - this.name = name; - this.database = database; - this.client = database.client; - } - - insertOne(doc: T): Promise<{ insertedId: string }> { - return this.callApi("insertOne", { document: doc }); - } - - insertMany(docs: T[]): Promise<{ insertedIds: string[] }> { - return this.callApi("insertMany", { documents: docs }); - } - - async findOne( - filter: Bson.Document, - { projection }: { projection?: Bson.Document } = {}, - ): Promise { - const result = await this.callApi("findOne", { - filter, - projection, - }); - return result.document; - } - - async find( - filter?: Bson.Document, - { projection, sort, limit, skip }: { - projection?: Bson.Document; - sort?: Bson.Document; - limit?: number; - skip?: number; - } = {}, - ): Promise { - const result = await this.callApi("find", { - filter, - projection, - sort, - limit, - skip, - }); - return result.documents; - } - - updateOne( - filter: Bson.Document, - update: Bson.Document, - { upsert }: { upsert?: boolean }, - ): Promise< - { matchedCount: number; modifiedCount: number; upsertedId?: string } - > { - return this.callApi("updateOne", { - filter, - update, - upsert, - }); - } - - updateMany( - filter: Bson.Document, - update: Bson.Document, - { upsert }: { upsert?: boolean }, - ): Promise< - { matchedCount: number; modifiedCount: number; upsertedId?: string } - > { - return this.callApi("updateMany", { - filter, - update, - upsert, - }); - } - - replaceOne( - filter: Bson.Document, - replacement: Bson.Document, - { upsert }: { upsert?: boolean }, - ): Promise< - { matchedCount: number; modifiedCount: number; upsertedId?: string } - > { - return this.callApi("replaceOne", { - filter, - replacement, - upsert, - }); - } - - deleteOne(filter: Bson.Document): Promise<{ deletedCount: number }> { - return this.callApi("deleteOne", { filter }); - } - - deleteMany(filter: Bson.Document): Promise<{ deletedCount: number }> { - return this.callApi("deleteMany", { filter }); - } - - async aggregate(pipeline: Bson.Document[]): Promise { - const result = await this.callApi("aggregate", { pipeline }); - return result.documents; - } - - async countDocuments( - filter?: Bson.Document, - options?: { limit?: number; skip?: number }, - ): Promise { - const pipeline: Bson.Document[] = []; - if (filter) { - pipeline.push({ $match: filter }); - } - - if (typeof options?.skip === "number") { - pipeline.push({ $skip: options.limit }); - } - - if (typeof options?.limit === "number") { - pipeline.push({ $limit: options.limit }); - } - - pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } }); - - const [result] = await this.aggregate<{ n: number }>(pipeline); - if (result) return result.n; - return 0; - } - - async estimatedDocumentCount(): Promise { - const pipeline = [ - { $collStats: { count: {} } }, - { $group: { _id: 1, n: { $sum: "$count" } } }, - ]; - - const [result] = await this.aggregate<{ n: number }>(pipeline); - if (result) return result.n; - return 0; - } - - async callApi(method: string, extra: Bson.Document) { - const { endpoint, appId, apiKey, dataSource } = this.client; - const url = `${endpoint}/app/${appId}/endpoint/data/beta/action/${method}`; - - const response = await this.client.fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "api-key": apiKey, - }, - body: Bson.EJSON.stringify({ - collection: this.name, - database: this.database.name, - dataSource: dataSource, - ...extra, - }), - }); - - if (response.ok) { - return response.json(); - } - - throw new Error(`${response.statusText}: ${await response.text()}`); - } -} +export { Bson } from "./deps.ts"; +export { MongoClient } from "./client.ts"; +export type { MongoClientConstructorOptions } from "./client.ts"; diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..a2783fd --- /dev/null +++ b/test.ts @@ -0,0 +1,53 @@ +// deno-lint-ignore-file require-await +import { Bson, MongoClient } from "./mod.ts"; +import { deferred } from "https://deno.land/std@0.135.0/async/deferred.ts"; +import { assertEquals } from "https://deno.land/std@0.135.0/testing/asserts.ts"; + +Deno.test("Sample Test", async () => { + const fetchMock = deferred<{ url: string; init: RequestInit }>(); + + const client = new MongoClient({ + appId: "appId", + dataSource: "dataSource", + apiKey: "API_KEY", + fetch: (async (url: string, init: RequestInit) => { + fetchMock.resolve({ url, init }); + return { + ok: true, + json: async () => ({ ok: true }), + }; + }) as typeof fetch, + }); + + const _id = new Bson.ObjectId(); + client.database("db-name").collection("c-name").insertOne({ + _id, + foo: "bar", + }); + + const { url, init } = await fetchMock; + assertEquals( + url, + "https://data.mongodb-api.com/app/appId/endpoint/data/beta/action/insertOne", + ); + assertEquals(init.method, "POST"); + assertEquals( + new Headers(init.headers).get("Content-Type"), + "application/json", + ); + assertEquals(new Headers(init.headers).get("api-key"), "API_KEY"); + assertEquals( + await new Request(url, init).json(), + { + collection: "c-name", + database: "db-name", + dataSource: "dataSource", + document: { + _id: { + $oid: _id.toHexString(), + }, + foo: "bar", + }, + }, + ); +});