diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3002255 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true +} \ No newline at end of file diff --git a/README.md b/README.md index b6d08e7..48d7af2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,131 @@ -# atlas_web -TypeSafe SDK for MongoDB Atlas data API - for Web/Deno +# Atlas + +> **atlas** is a TypeSafe **MongoDB Atlas Data API** SDK for Deno & Deno Deploy + +[![Discord server](https://img.shields.io/discord/768918486575480863?color=blue&label=Ask%20for%20help%20here&logo=discord&style=flat-square)](https://discord.gg/HEdTCvZUSf) + +## Links + +- [Docs](https://doc.deno.land/https/deno.land/x/atlas/mod.ts) + +### Import + +Replace `LATEST_VERSION` with +[current latest version](https://deno.land/x/atlas) + +```ts +import { + Bson, + MongoClient, +} from "https://deno.land/x/atlas@LATEST_VERSION/mod.ts"; +``` + +### Connect + +```ts +const client = new MongoClient({ + appId: "YOUR_APP_ID", + dataSource: "YOUR_CLUSTER_NAME", + apiKey: "YOUR_API_KEY", +}); +``` + +### Access Collection + +```ts +// Defining schema interface +interface UserSchema { + _id: ObjectId | string; + username: string; + password: string; +} + +const db = client.database("test"); +const users = db.collection("users"); +``` + +### Insert + +```ts +const insertId = await users.insertOne({ + username: "user1", + password: "pass1", +}); + +const insertIds = await users.insertMany([ + { + username: "user1", + password: "pass1", + }, + { + username: "user2", + password: "pass2", + }, +]); +``` + +### Find + +```ts +const user1 = await users.findOne({ _id: insertId }); + +const all_users = await users.find({ username: { $ne: null } }); + +// find by ObjectId +const user1_id = await users.findOne({ + _id: new ObjectId("SOME OBJECTID STRING"), +}); +``` + +### Count + +```ts +const count = await users.countDocuments({ username: { $ne: null } }); + +const estimatedCount = await users.estimatedDocumentCount({ + username: { $ne: null }, +}); +``` + +### Aggregation + +```ts +const docs = await users.aggregate([ + { $match: { username: "many" } }, + { $group: { _id: "$username", total: { $sum: 1 } } }, +]); +``` + +### Update + +```ts +const { matchedCount, modifiedCount, upsertedId } = await users.updateOne( + { username: { $ne: null } }, + { $set: { username: "USERNAME" } }, +); + +const { matchedCount, modifiedCount, upsertedId } = await users.updateMany( + { username: { $ne: null } }, + { $set: { username: "USERNAME" } }, +); +``` + +### Replace + +```ts +const { matchedCount, modifiedCount, upsertedId } = await users.replaceOne( + { username: "a" }, + { + username: "user1", + password: "pass1", + }, // new document +); +``` + +### Delete + +```ts +const deleteCount = await users.deleteOne({ _id: insertId }); + +const deleteCount2 = await users.deleteMany({ username: "test" }); +``` \ No newline at end of file diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..987521b --- /dev/null +++ b/deps.ts @@ -0,0 +1 @@ +export * as Bson from "https://esm.sh/bson@4.6.2"; diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..d09e96a --- /dev/null +++ b/mod.ts @@ -0,0 +1,217 @@ +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()}`); + } +}