Created
October 9, 2021 12:48
-
-
Save xieyuheng/31ae7af845f5d03eba71d4db24330b2c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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