Skip to content

Instantly share code, notes, and snippets.

@xieyuheng
Created October 9, 2021 12:48
Show Gist options
  • Save xieyuheng/31ae7af845f5d03eba71d4db24330b2c to your computer and use it in GitHub Desktop.
Save xieyuheng/31ae7af845f5d03eba71d4db24330b2c to your computer and use it in GitHub Desktop.
import { Resource } from "../resource"
import * as Mongo from "mongodb"
import ty, { Schema } from "@xieyuheng/ty"
import crypto from "crypto"
export abstract class MongoResource<T, Pk extends keyof T> extends Resource<
T,
Pk
> {
abstract name: string
abstract primaryKey: Pk
abstract schemas: { [P in keyof T]: Schema<T[P]> }
db: Mongo.Db
constructor(opts: { db: Mongo.Db }) {
super()
this.db = opts.db
}
get schema(): Schema<T> {
return ty.object(this.schemas)
}
get collection(): Mongo.Collection<T> {
return this.db.collection(this.name)
}
generatePrimaryKey(): string {
return crypto.randomBytes(32).toString("hex")
}
private createEntity(input: Omit<T, Pk>): T {
const pk = this.generatePrimaryKey()
const entity: any = { [this.primaryKey]: pk, _id: pk, ...input }
return entity
}
async create(input: Omit<T, Pk>): Promise<T> {
const entity = this.createEntity(input)
await this.collection.insertOne(entity as Mongo.OptionalId<T>)
return this.schema.prune(entity)
}
async createMany(inputs: Array<Omit<T, Pk>>): Promise<Array<T>> {
const entities = inputs.map((input) => this.createEntity(input))
await this.collection.insertMany(entities as Array<Mongo.OptionalId<T>>)
return entities.map((entity) => this.schema.prune(entity))
}
async get(pk: T[Pk]): Promise<T | undefined> {
const query = { _id: pk }
const result = await this.collection.findOne(query)
if (result !== null) {
return this.schema.prune(result)
} else {
return undefined
}
}
async find(query: Partial<T>): Promise<Array<T>> {
if (query[this.primaryKey]) {
query = { ...query, _id: query[this.primaryKey] }
}
const results = await this.collection.find(query).toArray()
return results.map((result) => this.schema.prune(result))
}
async all(): Promise<Array<T>> {
return await this.find({})
}
async patch(entity: Partial<T> & Pick<T, Pk>): Promise<boolean> {
const result = await this.collection.updateOne(
{ _id: entity[this.primaryKey] },
{ $set: entity },
{ upsert: true }
)
return result.matchedCount > 0
}
async delete(pk: T[Pk]): Promise<boolean> {
const query = { _id: pk }
const result = await this.collection.deleteOne(query)
return result.deletedCount > 0
}
async attach<
A extends Record<
string,
{ resource: MongoResource<any, string>; by: string }
>
>(
entities: Array<T>,
attachments: A
): Promise<
Array<
T & {
[P in keyof A]: Array<
ReturnType<A[P]["resource"]["schema"]["validate"]>
>
}
>
> {
for (const key in attachments) {
const { resource, by } = attachments[key]
const aggregated = await resource.collection
.aggregate([
{
$match: {
[by]: { $in: entities.map((entity) => entity[this.primaryKey]) },
},
},
{ $group: { _id: "$" + by, entities: { $addToSet: "$$ROOT" } } },
])
.toArray()
type Attachment = ReturnType<typeof resource["schema"]["validate"]>
const record: Record<string, Array<Attachment>> = {} as any
for (const { _id, entities } of aggregated) {
record[_id] = entities
}
for (const i in entities) {
const entity: T = entities[i]
entities[i] = {
...entity,
[by]: record[String(entity[this.primaryKey])],
}
}
}
return entities as any
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment