import { Db, ObjectId, Collection, CollectionCreateOptions } from "mongodb"; import { SchemaValidator, SchemaFieldValidationError } from "./Schema"; import { MongoCollection, ICollection } from "./MongoCollection"; export enum ERelationType { None, SingleObjectId, ArrayObjectId, }; export interface IFieldOptions { required?: boolean; // Is the field required? (Can not be null or undefined) unique?: boolean; // Is the field unique? hidden?: boolean; // Should the field visible when converted toJSON? // Relation fields foreignField?: string; // oneToOne from the other collection by this field autoPopulate?: boolean; // should the field auto populated? (Eager relation) relationType?: ERelationType; override?: Function; // Function to override the value when saving type?: any; // Type of the field (Override) index?: -1 | 0 | 1; // Is the field indexed? }; export interface IFieldIndex { specification: object; unique: boolean, }; export interface IDocumentOptions { indexes?: IFieldIndex[]; inherits?: any[] } export class MongoSchemaField { constructor(private fieldName, private fieldTypeFn: Function, private fieldOptions: IFieldOptions) { } get name() { return this.getName(); } get options() { return this.getOptions(); } get type() { return this.getType(); } getName(): string { return this.fieldName; } getType(): any { return this.fieldTypeFn(); } getOptions(): IFieldOptions { return this.fieldOptions; } getReferencedCollection() { if(this.fieldOptions.relationType == ERelationType.None) return null; return this.getType(); } getForeignFieldName() { return this.fieldOptions.foreignField; } }; export class MongoSchema { public name: string; public indexes: IFieldIndex[] = []; public fields: MongoSchemaField[] = []; protected DB: Db; public createOptions: CollectionCreateOptions; public hooks: any = {}; public inherits: typeof MongoCollection[] = []; constructor(public CollectionSpec: any) { // this.addField("_id", ObjectId, {}); // this.addField("_version", Number, { // }); } addField(fieldName: string, fieldType: any, fieldOptions: IFieldOptions) { if(fieldOptions.index !== undefined || fieldOptions.unique !== undefined ) { var index: IFieldIndex = { specification: {}, unique: fieldOptions.unique, }; index.specification[fieldName] = fieldOptions.index || 1; this.addIndex(index); } this.fields.push(new MongoSchemaField(fieldName, fieldType, fieldOptions)); } getHiddenFields() { return this.callHook("fields:hidden", this.fields.filter( field => field.options.hidden)); } getVisibleFields() { return this.callHook("fields:visible", this.fields.filter( field => !field.options.hidden )); } getAutoPopulateFields() { return this.callHook("fields:autopopulate", this.fields.filter( field => field.options.autoPopulate && field.getReferencedCollection() != undefined)); } getAutoPopulateFieldNamesRecursive() { let fields = []; let autopopulateFields = this.fields.filter( field => field.options.autoPopulate && field.getReferencedCollection() != undefined); for(let field of autopopulateFields) { let fieldChain = []; fieldChain.push(field.name); let childFields = field.getReferencedCollection().getSchema().getAutoPopulateFieldNamesRecursive(); fieldChain.push(...childFields); fields.push(fieldChain.join(".")); } return this.callHook("fields:autopopulate", fields); } addIndex(index: IFieldIndex) { this.indexes.push(index); } setConnection(db: Db) { this.DB = db; this.callHook("db:connected"); } getField(name: string) { return this.fields.find( field => { if(field.name == name) return true; return false; }); } prepare() { for(let inherited of this.inherits) { let inheritedSchema:MongoSchema = inherited.getSchema(); for(let inheritedField of inheritedSchema.fields) { // Skip fields. We allow overrides this way? if(this.getField(inheritedField.name)) continue; this.fields.unshift(inheritedField); } } } collection(): Collection { return this.DB.collection(this.name); } createCollection() { return this.DB.createCollection(this.name, this.createOptions).then( result => { this.callHook("collection:created"); return this.collection().indexes().then( async (indexes) => { for(let index of indexes) { if(index.name == "_id_") continue; await this.collection().dropIndex(index.name); } // Drop old indexes & create new ones for(let index of this.indexes) { this.collection().createIndex(index.specification, { unique: index.unique, dropDups: true, w: 1 }).catch(error => { throw error; }); } this.callHook("indexes:created"); }); }); } registerHook(hookName: string, callback: any) { this.hooks[hookName] = callback; } callHook(hookName: string, input?: any) { if(this.hooks[hookName]) { return this.hooks[hookName](input); } return input; } validate(document: object): Error { if(!document) { document = {}; } let validatingDocument = this.callHook("validate", document); for(var field of this.fields) { var error = SchemaValidator.validateField(field, validatingDocument[field.name]); if(error) { return error; } } return null; } };