import { MongoAdapter } from "../MongoAdapter"; import { MongoSchema, MongoSchemaField, ERelationType } from "./MongoSchema"; import { Field } from "./Decorators"; import { MongoQuery, MongoQueryMulti, MongoQuerySingle } from "./Query"; import { Db, Collection, Cursor, AggregationCursor, ObjectId, WriteOpResult, InsertOneWriteOpResult, UpdateWriteOpResult, CollectionInsertOneOptions, ReplaceOneOptions, DeleteWriteOpResultObject, ReplaceWriteOpResult, InsertWriteOpResult, CollStats, FindOneAndReplaceOption, FindAndModifyWriteOpResultObject } from "mongodb"; //import { IFindQuery } "./FindQuery"; import { MongoSchemaRegistry } from "./MongoSchemaRegistry"; import { ObjectID } from "bson"; import { DeepPartial, ObjectType } from "./Types"; export interface ICollection { getSchemaDefinition(): MongoSchema; }; export class MongoCollection implements ICollection { @Field() _id: ObjectId; //_version: number; get collectionName() { return this.constructor.name; } constructor(data?: any) { this.hydrate(data); } static constructCollection(type: new () => T): T { return new type(); } // Any into Entity hydrate(data: any) { if(!data) { data = {}; } // Make sure the ingested _id is an ObjectId MongoCollection.ensureObjectId(data); let schema = this.getSchemaDefinition(); for(let field of schema.fields) { let fieldName = field.getName(); let fieldValue = data[fieldName]; let fieldOptions = field.getOptions(); // If no data do nothing. if(data[fieldName] === undefined) { // But if override is available apply it if(fieldOptions.override && typeof fieldOptions.override === "function") { this[fieldName] = fieldOptions.override(this[fieldName]); } continue; } // If no relations to handle we just write the data! if(fieldOptions.relationType == ERelationType.None) { this[fieldName] = fieldValue; continue; } // Handle objectId if(fieldOptions.relationType == ERelationType.SingleObjectId) { this[fieldName] = this.hydrateSubDocument(fieldValue, field); continue; } // Handle array of objectids if(fieldOptions.relationType == ERelationType.ArrayObjectId) { // Ensure the data is an array! if(!Array.isArray(fieldValue)) { throw new Error("Data is not array for ArrayObjectId on: " + this.collectionName + "." + fieldName); } this[fieldName] = fieldValue.map((subDocument) => this.hydrateSubDocument(subDocument, field)); continue; } throw new Error("Unhandled hydration for field: " + this.collectionName + "." + fieldName); } } // Hydrates one subdocument hydrateSubDocument(fieldValue, field: MongoSchemaField) { if(!fieldValue._id) { // IF the field is a string, it should be converted into objectId first. if(typeof fieldValue === "string") { fieldValue = new ObjectID(fieldValue); } if(fieldValue instanceof ObjectId) { let collection = field.getReferencedCollection(); let referencedField = new collection(); referencedField._id = fieldValue; return referencedField; } // If we fail here something is wrong with the data! throw new Error("Invalid ObjectId"); } MongoCollection.ensureObjectId(fieldValue); let collection = field.getReferencedCollection(); let referencedField = new collection(); referencedField.hydrate(fieldValue); return referencedField; } // Creates a json friendly object dehydrate(fields: MongoSchemaField[], relationsToIds: boolean = false): object { let rawData = {}; for(let field of fields) { let fieldName = field.name; let fieldValue = this[fieldName]; let fieldOptions = field.options; // If no data do nothing. if(this[fieldName] === undefined) { // But if override is available apply it if(fieldOptions.override && typeof fieldOptions.override === "function") { rawData[fieldName] = fieldOptions.override(this[fieldName]); } continue; } if(relationsToIds) { // Convert the subdocument to an _id if(fieldOptions.relationType == ERelationType.SingleObjectId) { rawData[fieldName] = this[fieldName]._id; continue; } if(fieldOptions.relationType == ERelationType.ArrayObjectId) { rawData[fieldName] = this[fieldName].map((subdocument) => { return subdocument._id; }).filter( value => { return value != null; }); continue; } } rawData[fieldName] = this[fieldName]; } return rawData; } toMongoDocument() { let schema = this.getSchemaDefinition(); // Handle overrides (Only when saving) return this.dehydrate(schema.fields, true); } toObject(): object { let schema = this.getSchemaDefinition(); return this.dehydrate(schema.fields); } // Same as dehydrate but drops the hidden fields toJSON() { let schema = this.getSchemaDefinition(); return this.dehydrate(schema.getVisibleFields()); } getValidatedObject(): object { let schema = this.getSchemaDefinition(); let collection = schema.collection(); let data = this.toObject(); let validationResult = schema.validate(data); if(!validationResult) { return null; } return data; } validate(data?: any) { if(!data) { data = this.toMongoDocument(); } let schema = this.getSchemaDefinition(); return schema.validate(data); } private static ensureObjectId(query: any) { if(query && query._id && typeof query._id === "string") { query._id = new ObjectID(query._id); } } static query(this: ObjectType, query?: Object): MongoQuery< T > { MongoCollection.ensureObjectId(query); return new MongoQuery((this as any), query); } static find(this: ObjectType, query?: Object): MongoQueryMulti< T > { MongoCollection.ensureObjectId(query); return new MongoQueryMulti((this as any), query); } static findOne(this: ObjectType, query?: Object): MongoQuerySingle< T > { MongoCollection.ensureObjectId(query); return new MongoQuerySingle((this as any), query); } static createOne(this: ObjectType, data?: DeepPartial): Promise< T > { let instance = (this as any).constructCollection(this); instance.hydrate(data); return instance.save(); } static findOneAndUpdate(this: ObjectType, query?: Object, data?: any, options: FindOneAndReplaceOption = undefined): Promise { MongoCollection.ensureObjectId(query); let collection = (this as any).getSchema().collection(); let keys = Object.keys(data || {}); if(keys.findIndex((key) => key.startsWith("$")) >= 0) { return collection.findOneAndUpdate(query, data, options).then( result => { if(options && options.returnOriginal) { return result.value; } return result; }).catch( error => { return error; }) }else{ return collection.findOneAndUpdate(query, { $set: data }, options).then( result => { if(options && options.returnOriginal) { return result.value; } return result; }).catch( error => { return error; }) } } static updateOne(this: ObjectType, query?: Object, data?: any, options: ReplaceOneOptions = undefined): Promise { MongoCollection.ensureObjectId(query); let collection = (this as any).getSchema().collection(); let keys = Object.keys(data || {}); if(keys.findIndex((key) => key.startsWith("$")) >= 0) { return collection.updateOne(query, data, options); }else{ return collection.updateOne(query, { $set: data }, options); } } static update(this: ObjectType, query?: Object, data?: any, options: ReplaceOneOptions & { multi?: boolean } = undefined): Promise { MongoCollection.ensureObjectId(query); let collection = (this as any).getSchema().collection(); let keys = Object.keys(data || {}); if(keys.findIndex((key) => key.startsWith("$")) >= 0) { return collection.update(query, data, options); }else{ return collection.update(query, { $set: data }, options); } } static remove(query: Object): Promise { MongoCollection.ensureObjectId(query); let collection = this.getSchema().collection(); return collection.remove(query); } static removeOne(query: Object): Promise { MongoCollection.ensureObjectId(query); let collection = this.getSchema().collection(); return collection.remove(query, {single: true}); } static aggregate(pipeline:any[] = []): AggregationCursor { return this.getCollection().aggregate(pipeline); } static getSchema(): MongoSchema { return MongoSchemaRegistry.getSchema(this.name); } static getCollection(): Collection { return this.getSchema().collection(); } static stats(): Promise { return this.getCollection().stats(); } collection(): Collection { return this.getSchemaDefinition().collection(); } getSchemaDefinition(): MongoSchema { return MongoSchemaRegistry.getSchema(this.constructor.name); //return (this.constructor).SchemaDefinition; } // reloads the document reload() { return this.load(); } // loads the object if id is available load() { return new MongoQuerySingle((this.constructor as any), { _id: this._id }).then( result => { this.hydrate(result.toObject()); return result; }).catch( error => { return error; }); } save(options: any = {}): Promise { if(!this._id) { return this.insert(options); }else{ return this.update(options); } } insert(options: CollectionInsertOneOptions = undefined): Promise { let schema = this.getSchemaDefinition(); let collection = schema.collection(); let data = this.toMongoDocument(); let validationResult = schema.validate(data); if(!validationResult) { return collection.insertOne(data, options).then( result => { this._id = result.insertedId; // Assign inserted _id return this; }); } return Promise.reject(validationResult); } update(options: ReplaceOneOptions = undefined): Promise { let schema = this.getSchemaDefinition(); let collection = schema.collection(); let data = this.toMongoDocument(); let validationResult = schema.validate(data); if(!validationResult) { return collection.updateOne({ _id: this._id, }, { $set: data }, options).then( result => { return this; }); } return Promise.reject(validationResult); } remove(): Promise { if(!this._id) { return Promise.reject("_id is not defined"); } return this.collection().deleteOne({ _id: this._id }); } replace(replaceWith: MongoCollection): Promise { let schema = replaceWith.getSchemaDefinition(); let collection = schema.collection(); let data = replaceWith.toMongoDocument(); let validationResult = schema.validate(data); if(!validationResult) { return this.collection().replaceOne({ _id: this._id }, data); } return Promise.reject(validationResult); } };