diff --git a/.gitignore b/.gitignore index 5f71bfcc..f3761790 100644 --- a/.gitignore +++ b/.gitignore @@ -8,18 +8,21 @@ # libraries node_modules -# testing -coverage -.nyc_output - #logs npm-debug.log +# build **/*.js **/*.js.map **/*.d.ts + +# src !lib/models/Model.d.ts !lib/models/Model.js !lib/models/Sequelize.d.ts !lib/models/Sequelize.js +# testing +coverage +.nyc_output +!test/tsconfig.mocha.js diff --git a/example/app.ts b/example/app.ts index 7c1f1fa3..6618e8f3 100644 --- a/example/app.ts +++ b/example/app.ts @@ -10,7 +10,7 @@ import Book from "./models/Book"; /* tslint:disable:no-console */ /* tslint:disable:no-unused-new */ -new Sequelize({ +const s = new Sequelize({ validateOnly: true, modelPaths: [__dirname + '/models/validation-only'] }); diff --git a/index.ts b/index.ts index abae4d5b..d387b40a 100644 --- a/index.ts +++ b/index.ts @@ -98,7 +98,6 @@ export {IScopeFindOptions} from "./lib/interfaces/IScopeFindOptions"; export {IScopeIncludeAssociation} from "./lib/interfaces/IScopeIncludeAssociation"; export {IScopeIncludeOptions} from "./lib/interfaces/IScopeIncludeOptions"; export {IScopeOptions} from "./lib/interfaces/IScopeOptions"; -export {ISequelizeAssociation} from "./lib/interfaces/ISequelizeAssociation"; export {ISequelizeConfig} from "./lib/interfaces/ISequelizeConfig"; export {ISequelizeForeignKeyConfig} from "./lib/interfaces/ISequelizeForeignKeyConfig"; export {ISequelizeValidationOnlyConfig} from "./lib/interfaces/ISequelizeValidationOnlyConfig"; diff --git a/lib/annotations/Column.ts b/lib/annotations/Column.ts index 710daff7..40d1fc52 100644 --- a/lib/annotations/Column.ts +++ b/lib/annotations/Column.ts @@ -40,7 +40,7 @@ function annotate(target: any, }; } else { - options = Object.assign({}, optionsOrDataType); + options = Object.assign({}, optionsOrDataType as IPartialDefineAttributeColumnOptions); if (!options.type) { options.type = getSequelizeTypeByDesignType(target, propertyName); diff --git a/lib/annotations/association/BelongsTo.ts b/lib/annotations/association/BelongsTo.ts index 05a93c25..890c3a79 100644 --- a/lib/annotations/association/BelongsTo.ts +++ b/lib/annotations/association/BelongsTo.ts @@ -1,21 +1,22 @@ import {AssociationOptionsBelongsTo} from 'sequelize'; -import {BELONGS_TO, addAssociation} from "../../services/association"; +import {addAssociation, getPreparedAssociationOptions} from "../../services/association"; import {ModelClassGetter} from "../../types/ModelClassGetter"; +import {BelongsToAssociation} from '../../models/association/BelongsToAssociation'; -export function BelongsTo(relatedClassGetter: ModelClassGetter, +export function BelongsTo(associatedClassGetter: ModelClassGetter, foreignKey?: string): Function; -export function BelongsTo(relatedClassGetter: ModelClassGetter, +export function BelongsTo(associatedClassGetter: ModelClassGetter, options?: AssociationOptionsBelongsTo): Function; -export function BelongsTo(relatedClassGetter: ModelClassGetter, +export function BelongsTo(associatedClassGetter: ModelClassGetter, optionsOrForeignKey?: string | AssociationOptionsBelongsTo): Function { return (target: any, propertyName: string) => { - addAssociation( - target, - BELONGS_TO, - relatedClassGetter, - propertyName, - optionsOrForeignKey + const options: AssociationOptionsBelongsTo = getPreparedAssociationOptions(optionsOrForeignKey); + if (!options.as) options.as = propertyName; + addAssociation(target, new BelongsToAssociation( + associatedClassGetter, + options, + ) ); }; } diff --git a/lib/annotations/association/BelongsToMany.ts b/lib/annotations/association/BelongsToMany.ts index 27fbbcaf..33cbc82d 100644 --- a/lib/annotations/association/BelongsToMany.ts +++ b/lib/annotations/association/BelongsToMany.ts @@ -1,36 +1,35 @@ -import {BELONGS_TO_MANY, addAssociation} from "../../services/association"; +import {addAssociation} from "../../services/association"; import {ModelClassGetter} from "../../types/ModelClassGetter"; import {IAssociationOptionsBelongsToMany} from "../../interfaces/IAssociationOptionsBelongsToMany"; +import {BelongsToManyAssociation} from '../../models/association/BelongsToManyAssociation'; -export function BelongsToMany(relatedClassGetter: ModelClassGetter, - through: (ModelClassGetter) | string, +export function BelongsToMany(associatedClassGetter: ModelClassGetter, + through: ModelClassGetter | string, foreignKey?: string, otherKey?: string): Function; -export function BelongsToMany(relatedClassGetter: ModelClassGetter, +export function BelongsToMany(associatedClassGetter: ModelClassGetter, options: IAssociationOptionsBelongsToMany): Function; -export function BelongsToMany(relatedClassGetter: ModelClassGetter, - throughOrOptions: (ModelClassGetter | string) | IAssociationOptionsBelongsToMany, +export function BelongsToMany(associatedClassGetter: ModelClassGetter, + throughOrOptions: ModelClassGetter | string | IAssociationOptionsBelongsToMany, foreignKey?: string, otherKey?: string): Function { - const typeOfThroughOrOptions = typeof throughOrOptions; - let through; - let options: Partial; - if (typeOfThroughOrOptions === 'string' || typeOfThroughOrOptions === 'function') { - through = throughOrOptions; - } else { - through = (throughOrOptions as IAssociationOptionsBelongsToMany).through; - options = throughOrOptions as IAssociationOptionsBelongsToMany; - } return (target: any, propertyName: string) => { - addAssociation( - target, - BELONGS_TO_MANY, - relatedClassGetter, - propertyName, - options || foreignKey, - through, - otherKey, + let options: Partial = {foreignKey, otherKey}; + + if (typeof throughOrOptions === 'string' || + typeof throughOrOptions === 'function') { + options.through = throughOrOptions; + } else { + options = {...throughOrOptions}; + } + + if (!options.as) options.as = propertyName; + + addAssociation(target, new BelongsToManyAssociation( + associatedClassGetter, + options as IAssociationOptionsBelongsToMany, + ) ); }; } diff --git a/lib/annotations/association/HasMany.ts b/lib/annotations/association/HasMany.ts index f49c808a..7c67f37e 100644 --- a/lib/annotations/association/HasMany.ts +++ b/lib/annotations/association/HasMany.ts @@ -1,20 +1,24 @@ import {AssociationOptionsHasMany} from 'sequelize'; -import {HAS_MANY, addAssociation} from "../../services/association"; +import {addAssociation, getPreparedAssociationOptions} from "../../services/association"; +import {Association} from "../../enums/Association"; import {ModelClassGetter} from "../../types/ModelClassGetter"; +import {HasAssociation} from '../../models/association/HasAssociation'; -export function HasMany(relatedClassGetter: ModelClassGetter, +export function HasMany(associatedClassGetter: ModelClassGetter, foreignKey?: string): Function; -export function HasMany(relatedClassGetter: ModelClassGetter, +export function HasMany(associatedClassGetter: ModelClassGetter, options?: AssociationOptionsHasMany): Function; -export function HasMany(relatedClassGetter: ModelClassGetter, +export function HasMany(associatedClassGetter: ModelClassGetter, optionsOrForeignKey?: string | AssociationOptionsHasMany): Function { + return (target: any, propertyName: string) => { - addAssociation( - target, - HAS_MANY, - relatedClassGetter, - propertyName, - optionsOrForeignKey, + const options: AssociationOptionsHasMany = getPreparedAssociationOptions(optionsOrForeignKey); + if (!options.as) options.as = propertyName; + addAssociation(target, new HasAssociation( + associatedClassGetter, + options, + Association.HasMany, + ) ); }; } diff --git a/lib/annotations/association/HasOne.ts b/lib/annotations/association/HasOne.ts index ab2e53cf..84f5e997 100644 --- a/lib/annotations/association/HasOne.ts +++ b/lib/annotations/association/HasOne.ts @@ -1,20 +1,24 @@ import {AssociationOptionsHasOne} from 'sequelize'; -import {addAssociation, HAS_ONE} from "../../services/association"; +import {Association} from "../../enums/Association"; import {ModelClassGetter} from "../../types/ModelClassGetter"; +import {addAssociation, getPreparedAssociationOptions} from '../../services/association'; +import {HasAssociation} from '../../models/association/HasAssociation'; -export function HasOne(relatedClassGetter: ModelClassGetter, +export function HasOne(associatedClassGetter: ModelClassGetter, foreignKey?: string): Function; -export function HasOne(relatedClassGetter: ModelClassGetter, +export function HasOne(associatedClassGetter: ModelClassGetter, options?: AssociationOptionsHasOne): Function; -export function HasOne(relatedClassGetter: ModelClassGetter, +export function HasOne(associatedClassGetter: ModelClassGetter, optionsOrForeignKey?: string | AssociationOptionsHasOne): Function { + return (target: any, propertyName: string) => { - addAssociation( - target, - HAS_ONE, - relatedClassGetter, - propertyName, - optionsOrForeignKey, + const options: AssociationOptionsHasOne = getPreparedAssociationOptions(optionsOrForeignKey); + if (!options.as) options.as = propertyName; + addAssociation(target, new HasAssociation( + associatedClassGetter, + options, + Association.HasOne, + ) ); }; } diff --git a/lib/enums/Association.ts b/lib/enums/Association.ts new file mode 100644 index 00000000..3bf5a0b4 --- /dev/null +++ b/lib/enums/Association.ts @@ -0,0 +1,6 @@ +export enum Association { + BelongsToMany = 'belongsToMany', + BelongsTo = 'belongsTo', + HasMany = 'hasMany', + HasOne = 'hasOne', +} diff --git a/lib/interfaces/AssociationOptions.ts b/lib/interfaces/AssociationOptions.ts new file mode 100644 index 00000000..d06d776a --- /dev/null +++ b/lib/interfaces/AssociationOptions.ts @@ -0,0 +1,12 @@ +import { + AssociationOptionsBelongsTo, AssociationOptionsHasMany, + AssociationOptionsHasOne, AssociationOptionsManyToMany +} from 'sequelize'; +import {IPreparedAssociationOptionsBelongsToMany} from './IPreparedAssociationOptionsBelongsToMany'; + +export type AssociationOptions = + AssociationOptionsBelongsTo | + IPreparedAssociationOptionsBelongsToMany | + AssociationOptionsHasMany | + AssociationOptionsHasOne | + AssociationOptionsManyToMany; diff --git a/lib/interfaces/IAssociationOptionsBelongsToMany.ts b/lib/interfaces/IAssociationOptionsBelongsToMany.ts index 8d550293..3a3161c1 100644 --- a/lib/interfaces/IAssociationOptionsBelongsToMany.ts +++ b/lib/interfaces/IAssociationOptionsBelongsToMany.ts @@ -1,7 +1,8 @@ import {AssociationForeignKeyOptions, AssociationOptionsManyToMany} from "sequelize"; import {ModelClassGetter} from "../types/ModelClassGetter"; +import {IThroughOptions} from './IThroughOptions'; export interface IAssociationOptionsBelongsToMany extends AssociationOptionsManyToMany { - through: ModelClassGetter | string; + through: ModelClassGetter | string | IThroughOptions; otherKey?: string | AssociationForeignKeyOptions; } diff --git a/lib/interfaces/IFindOptions.ts b/lib/interfaces/IFindOptions.ts index 366803cc..5c7c0a7e 100644 --- a/lib/interfaces/IFindOptions.ts +++ b/lib/interfaces/IFindOptions.ts @@ -1,5 +1,7 @@ -import {WhereOptions, LoggingOptions, SearchPathOptions, col, FindOptionsAttributesArray, - literal, fn} from 'sequelize'; +import { + WhereOptions, LoggingOptions, SearchPathOptions, col, FindOptionsAttributesArray, + literal, fn, and, or +} from 'sequelize'; import {Model} from "../models/Model"; import {IIncludeOptions} from "./IIncludeOptions"; @@ -11,7 +13,7 @@ export interface IFindOptions extends LoggingOptions, SearchPathOptions { /** * A hash of attributes to describe your search. See above for examples. */ - where?: WhereOptions; + where?: WhereOptions | fn | or | Array; /** * A list of the attributes that you want to select. To rename an attribute, you can pass an array, with diff --git a/lib/interfaces/IPreparedAssociationOptionsBelongsToMany.ts b/lib/interfaces/IPreparedAssociationOptionsBelongsToMany.ts new file mode 100644 index 00000000..4936be4a --- /dev/null +++ b/lib/interfaces/IPreparedAssociationOptionsBelongsToMany.ts @@ -0,0 +1,7 @@ +import {AssociationForeignKeyOptions, AssociationOptionsManyToMany} from "sequelize"; +import {IPreparedThroughOptions} from './IPreparedThroughOptions'; + +export interface IPreparedAssociationOptionsBelongsToMany extends AssociationOptionsManyToMany { + through: IPreparedThroughOptions; + otherKey?: string | AssociationForeignKeyOptions; +} diff --git a/lib/interfaces/IPreparedThroughOptions.ts b/lib/interfaces/IPreparedThroughOptions.ts new file mode 100644 index 00000000..b118b38d --- /dev/null +++ b/lib/interfaces/IPreparedThroughOptions.ts @@ -0,0 +1,30 @@ +import {AssociationScope} from 'sequelize'; +import {Model} from '../models/Model'; + +/** + * Used for a association table in n:m associations. + * + * @see AssociationOptionsBelongsToMany + */ +export interface IPreparedThroughOptions { + + /** + * The model used to join both sides of the N:M association. + */ + model: typeof Model; + + /** + * A key/value set that will be used for association create and find defaults on the through model. + * (Remember to add the attributes to the through model) + */ + scope?: AssociationScope; + + /** + * If true a unique key will be generated from the foreign keys used (might want to turn this off and create + * specific unique keys when using scopes) + * + * Defaults to true + */ + unique?: boolean; + +} diff --git a/lib/interfaces/ISequelizeAssociation.ts b/lib/interfaces/ISequelizeAssociation.ts deleted file mode 100644 index d9398fff..00000000 --- a/lib/interfaces/ISequelizeAssociation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {AssociationOptionsBelongsTo, AssociationOptionsBelongsToMany, AssociationOptionsHasMany, - AssociationOptionsHasOne, AssociationOptionsManyToMany} from 'sequelize'; -import {ModelClassGetter} from "../types/ModelClassGetter"; - -export interface ISequelizeAssociation { - - relation: string; - relatedClassGetter: ModelClassGetter; - through?: string; - throughClassGetter?: ModelClassGetter; - options?: AssociationOptionsBelongsTo | AssociationOptionsBelongsToMany | AssociationOptionsHasMany | - AssociationOptionsHasOne | AssociationOptionsManyToMany; - as: string; -} diff --git a/lib/interfaces/IThroughOptions.ts b/lib/interfaces/IThroughOptions.ts new file mode 100644 index 00000000..3034365c --- /dev/null +++ b/lib/interfaces/IThroughOptions.ts @@ -0,0 +1,30 @@ +import {AssociationScope} from 'sequelize'; +import {ModelClassGetter} from '../types/ModelClassGetter'; + +/** + * Used for a association table in n:m associations. + * + * @see AssociationOptionsBelongsToMany + */ +export interface IThroughOptions { + + /** + * The model used to join both sides of the N:M association. + */ + model: ModelClassGetter | string; + + /** + * A key/value set that will be used for association create and find defaults on the through model. + * (Remember to add the attributes to the through model) + */ + scope?: AssociationScope; + + /** + * If true a unique key will be generated from the foreign keys used (might want to turn this off and create + * specific unique keys when using scopes) + * + * Defaults to true + */ + unique?: boolean; + +} diff --git a/lib/models/BaseSequelize.ts b/lib/models/BaseSequelize.ts index 0a1c6ab6..26a5dd50 100644 --- a/lib/models/BaseSequelize.ts +++ b/lib/models/BaseSequelize.ts @@ -1,6 +1,6 @@ import {Model} from "./Model"; import {DEFAULT_DEFINE_OPTIONS, getModels} from "../services/models"; -import {getAssociations, processAssociation} from "../services/association"; +import {getAssociations} from "../services/association"; import {ISequelizeConfig} from "../interfaces/ISequelizeConfig"; import {ISequelizeUriConfig} from "../interfaces/ISequelizeUriConfig"; import {ISequelizeDbNameConfig} from "../interfaces/ISequelizeDbNameConfig"; @@ -9,7 +9,7 @@ import {resolveScopes} from "../services/scopes"; import {installHooks} from "../services/hooks"; import {ISequelizeValidationOnlyConfig} from "../interfaces/ISequelizeValidationOnlyConfig"; import {extend} from "../utils/object"; -import {ISequelizeAssociation} from "../interfaces/ISequelizeAssociation"; +import {BaseAssociation} from './association/BaseAssociation'; /** * Why does v3/Sequlize and v4/Sequelize does not extend? Because of @@ -99,7 +99,14 @@ export abstract class BaseSequelize { if (!associations) return; - associations.forEach(association => processAssociation(this, model, association)); + associations.forEach(association => { + association.init(model, this); + const associatedClass = association.getAssociatedClass(); + const relation = association.getAssociation(); + const options = association.getSequelizeOptions(); + model[relation](associatedClass, options); + this.adjustAssociation(model, association); + }); }); } @@ -110,7 +117,7 @@ export abstract class BaseSequelize { */ abstract getThroughModel(through: string): typeof Model; - abstract adjustAssociation(model: any, association: ISequelizeAssociation): void; + abstract adjustAssociation(model: any, association: BaseAssociation): void; abstract defineModels(models: Array): void; diff --git a/lib/models/association/BaseAssociation.ts b/lib/models/association/BaseAssociation.ts new file mode 100644 index 00000000..893930f2 --- /dev/null +++ b/lib/models/association/BaseAssociation.ts @@ -0,0 +1,59 @@ +import {AssociationOptions} from '../../interfaces/AssociationOptions'; +import {getForeignKeys} from '../../services/association'; +import {Model} from '../Model'; +import {AssociationForeignKeyOptions} from 'sequelize'; +import {BaseSequelize} from '../BaseSequelize'; +import {Association} from '../../enums/Association'; + +export abstract class BaseAssociation { + + private _options: AssociationOptions; + + abstract getAssociation(): Association; + + abstract getAssociatedClass(): typeof Model; + + protected abstract getPreparedOptions(model: typeof Model, + sequelize: BaseSequelize): AssociationOptions; + + init(model: typeof Model, + sequelize: BaseSequelize): void { + if (!this._options) { + this._options = this.getPreparedOptions(model, sequelize); + } + } + + getSequelizeOptions(): AssociationOptions { + if (!this._options) { + throw new Error(`Association need to be initialized with a sequelize instance`); + } + return this._options; + } + + protected getForeignKeyOptions(relatedClass: typeof Model, + classWithForeignKey: typeof Model, + foreignKey?: string | AssociationForeignKeyOptions): AssociationForeignKeyOptions { + let foreignKeyOptions: AssociationForeignKeyOptions = {}; + + if (typeof foreignKey === 'string') { + foreignKeyOptions.name = foreignKey; + } else if (foreignKey && typeof foreignKey === 'object') { + foreignKeyOptions = {...foreignKey}; + } + if (!foreignKeyOptions.name) { + const foreignKeys = getForeignKeys(classWithForeignKey.prototype) || []; + for (const key of foreignKeys) { + if (key.relatedClassGetter() === relatedClass) { + foreignKeyOptions.name = key.foreignKey; + break; + } + } + if (!foreignKeyOptions.name) { + throw new Error(`Foreign key for "${(relatedClass as any).name}" is missing ` + + `on "${(classWithForeignKey as any).name}".`); + } + } + + return foreignKeyOptions; + } +} diff --git a/lib/models/association/BelongsToAssociation.ts b/lib/models/association/BelongsToAssociation.ts new file mode 100644 index 00000000..ac17968e --- /dev/null +++ b/lib/models/association/BelongsToAssociation.ts @@ -0,0 +1,33 @@ +import {BaseAssociation} from './BaseAssociation'; +import {Model} from '../Model'; +import {BaseSequelize} from '../BaseSequelize'; +import {AssociationOptions} from '../../interfaces/AssociationOptions'; +import {ModelClassGetter} from '../../types/ModelClassGetter'; +import {AssociationOptionsBelongsTo} from 'sequelize'; +import {Association} from '../../enums/Association'; + +export class BelongsToAssociation extends BaseAssociation { + + constructor(private associatedClassGetter: ModelClassGetter, + private options: AssociationOptionsBelongsTo) { + super(); + } + + getAssociation(): Association { + return Association.BelongsTo; + } + + getAssociatedClass(): typeof Model { + return this.associatedClassGetter(); + } + + protected getPreparedOptions(model: typeof Model, + sequelize: BaseSequelize): AssociationOptions { + const options = {...this.options}; + const associatedClass = this.associatedClassGetter(); + + options.foreignKey = this.getForeignKeyOptions(associatedClass, model, options.foreignKey); + + return options; + } +} diff --git a/lib/models/association/BelongsToManyAssociation.ts b/lib/models/association/BelongsToManyAssociation.ts new file mode 100644 index 00000000..7b89c0d2 --- /dev/null +++ b/lib/models/association/BelongsToManyAssociation.ts @@ -0,0 +1,60 @@ +import {ModelClassGetter} from '../../types/ModelClassGetter'; +import {BaseAssociation} from './BaseAssociation'; +import {AssociationOptions} from '../../interfaces/AssociationOptions'; +import {BaseSequelize} from '../BaseSequelize'; +import {Model} from '../Model'; +import {Association} from '../../enums/Association'; +import {IAssociationOptionsBelongsToMany} from '../../interfaces/IAssociationOptionsBelongsToMany'; +import {IPreparedThroughOptions} from '../../interfaces/IPreparedThroughOptions'; +import {IPreparedAssociationOptionsBelongsToMany} from '../../interfaces/IPreparedAssociationOptionsBelongsToMany'; + +export class BelongsToManyAssociation extends BaseAssociation { + + constructor(private associatedClassGetter: ModelClassGetter, + private options: IAssociationOptionsBelongsToMany) { + super(); + } + + getAssociation(): Association { + return Association.BelongsToMany; + } + + getAssociatedClass(): typeof Model { + return this.associatedClassGetter(); + } + + protected getPreparedOptions(modelClass: typeof Model, + sequelize: BaseSequelize): AssociationOptions { + const options: IPreparedAssociationOptionsBelongsToMany = {...this.options as any}; + const associatedClass = this.associatedClassGetter(); + const throughOptions = this.getThroughOptions(modelClass, sequelize); + + options.through = throughOptions; + options.foreignKey = this.getForeignKeyOptions(modelClass, throughOptions.model, this.options.foreignKey); + options.otherKey = this.getForeignKeyOptions(associatedClass, throughOptions.model, this.options.otherKey); + + return options; + } + + private getThroughOptions(modelClass: typeof Model, + sequelize: BaseSequelize): IPreparedThroughOptions { + const through = this.options.through; + const model = typeof through === 'object' ? through.model : through; + const throughOptions: IPreparedThroughOptions = + typeof through === 'object' ? {...through} : {} as any; + + if (typeof model === 'function') { + throughOptions.model = model(); + } else if (typeof model === 'string') { + if (!sequelize.throughMap[model]) { + const throughModel = sequelize.getThroughModel(model); + sequelize.addModels([throughModel]); + sequelize.throughMap[model] = throughModel; + } + throughOptions.model = sequelize.throughMap[model]; + } else { + throw new Error(`Through model is missing for belongs to many association on ${modelClass.name}`); + } + return throughOptions; + } +} diff --git a/lib/models/association/HasAssociation.ts b/lib/models/association/HasAssociation.ts new file mode 100644 index 00000000..a6e44cc7 --- /dev/null +++ b/lib/models/association/HasAssociation.ts @@ -0,0 +1,34 @@ +import {BaseAssociation} from './BaseAssociation'; +import {Model} from '../Model'; +import {BaseSequelize} from '../BaseSequelize'; +import {AssociationOptions} from '../../interfaces/AssociationOptions'; +import {ModelClassGetter} from '../../types/ModelClassGetter'; +import {AssociationOptionsHasMany, AssociationOptionsHasOne} from 'sequelize'; +import {Association} from '../../enums/Association'; + +export class HasAssociation extends BaseAssociation { + + constructor(private associatedClassGetter: ModelClassGetter, + private options: AssociationOptionsHasMany | AssociationOptionsHasOne, + private association: Association) { + super(); + } + + getAssociation(): Association { + return this.association; + } + + getAssociatedClass(): typeof Model { + return this.associatedClassGetter(); + } + + protected getPreparedOptions(model: typeof Model, + sequelize: BaseSequelize): AssociationOptions { + const options = {...this.options}; + const associatedClass = this.associatedClassGetter(); + + options.foreignKey = this.getForeignKeyOptions(model, associatedClass, options.foreignKey); + + return options; + } +} diff --git a/lib/models/v3/Sequelize.ts b/lib/models/v3/Sequelize.ts index 4ce71cfb..77c96d20 100644 --- a/lib/models/v3/Sequelize.ts +++ b/lib/models/v3/Sequelize.ts @@ -6,7 +6,8 @@ import {getModelName, getAttributes, getOptions} from "../../services/models"; import {PROPERTY_LINK_TO_ORIG} from "../../services/models"; import {BaseSequelize} from "../BaseSequelize"; import {Table} from "../../annotations/Table"; -import {ISequelizeAssociation} from "../../interfaces/ISequelizeAssociation"; +import {BaseAssociation} from '../association/BaseAssociation'; +import {IPreparedAssociationOptionsBelongsToMany} from '../../interfaces/IPreparedAssociationOptionsBelongsToMany'; export class Sequelize extends SequelizeOrigin implements BaseSequelize { @@ -52,21 +53,25 @@ export class Sequelize extends SequelizeOrigin implements BaseSequelize { * The association needs to be adjusted. So that throughModel properties * referencing a original sequelize Model instance */ - adjustAssociation(model: any, association: ISequelizeAssociation): void { + adjustAssociation(model: any, association: BaseAssociation): void { + const options = association.getSequelizeOptions(); // The associations has to be adjusted - const internalAssociation = model['associations'][association.as]; + const internalAssociation = model['associations'][options.as as string]; // String based through's need adjustment if (internalAssociation.oneFromSource && internalAssociation.oneFromSource.as === 'Through') { + const belongsToManyOptions = options as IPreparedAssociationOptionsBelongsToMany; + const tableName = belongsToManyOptions.through.model.getTableName(); + // as and associationAccessor values referring to string "Through" - internalAssociation.oneFromSource.as = association.through; - internalAssociation.oneFromSource.options.as = association.through; - internalAssociation.oneFromSource.associationAccessor = association.through; - internalAssociation.oneFromTarget.as = association.through; - internalAssociation.oneFromTarget.options.as = association.through; - internalAssociation.oneFromTarget.associationAccessor = association.through; + internalAssociation.oneFromSource.as = tableName; + internalAssociation.oneFromSource.options.as = tableName; + internalAssociation.oneFromSource.associationAccessor = tableName; + internalAssociation.oneFromTarget.as = tableName; + internalAssociation.oneFromTarget.options.as = tableName; + internalAssociation.oneFromTarget.associationAccessor = tableName; } if (internalAssociation.throughModel && internalAssociation.throughModel.Model) { diff --git a/lib/models/v4/Sequelize.ts b/lib/models/v4/Sequelize.ts index 8359396b..67b28143 100644 --- a/lib/models/v4/Sequelize.ts +++ b/lib/models/v4/Sequelize.ts @@ -5,7 +5,7 @@ import {SequelizeConfig} from "../../types/SequelizeConfig"; import {getModelName, getAttributes, getOptions} from "../../services/models"; import {BaseSequelize} from "../BaseSequelize"; import {Table} from "../../annotations/Table"; -import {ISequelizeAssociation} from "../../interfaces/ISequelizeAssociation"; +import {BaseAssociation} from '../association/BaseAssociation'; export class Sequelize extends OriginSequelize implements BaseSequelize { @@ -43,7 +43,7 @@ export class Sequelize extends OriginSequelize implements BaseSequelize { return Through; } - adjustAssociation(model: any, association: ISequelizeAssociation): void { + adjustAssociation(model: any, association: BaseAssociation): void { } /** diff --git a/lib/services/association.ts b/lib/services/association.ts index 2302ec1a..4270e92d 100644 --- a/lib/services/association.ts +++ b/lib/services/association.ts @@ -1,146 +1,68 @@ import 'reflect-metadata'; -import {merge} from 'lodash'; import { - AssociationOptions, AssociationOptionsBelongsTo, AssociationOptionsBelongsToMany, - AssociationOptionsHasMany, AssociationOptionsHasOne, AssociationOptionsManyToMany + AssociationOptionsBelongsTo, AssociationOptionsHasMany, AssociationOptionsHasOne, AssociationOptionsManyToMany } from 'sequelize'; -import {Model} from "../models/Model"; import {ISequelizeForeignKeyConfig} from "../interfaces/ISequelizeForeignKeyConfig"; -import {ISequelizeAssociation} from "../interfaces/ISequelizeAssociation"; -import {BaseSequelize} from "../models/BaseSequelize"; import {ModelClassGetter} from "../types/ModelClassGetter"; -import {IAssociationOptionsBelongsToMany} from "../interfaces/IAssociationOptionsBelongsToMany"; - -export const BELONGS_TO_MANY = 'belongsToMany'; -export const BELONGS_TO = 'belongsTo'; -export const HAS_MANY = 'hasMany'; -export const HAS_ONE = 'hasOne'; +import {BaseAssociation} from '../models/association/BaseAssociation'; const FOREIGN_KEYS_KEY = 'sequelize:foreignKeys'; const ASSOCIATIONS_KEY = 'sequelize:associations'; -export type ConcatAssociationOptions = AssociationOptionsBelongsTo | - AssociationOptionsBelongsToMany | AssociationOptionsHasMany | - AssociationOptionsHasOne | AssociationOptionsManyToMany; +export type NonBelongsToManyAssociationOptions = + AssociationOptionsBelongsTo | + AssociationOptionsHasMany | + AssociationOptionsHasOne | + AssociationOptionsManyToMany; + +// tslint:disable:max-line-length +export function getPreparedAssociationOptions(optionsOrForeignKey?: string | NonBelongsToManyAssociationOptions): NonBelongsToManyAssociationOptions { + let options: NonBelongsToManyAssociationOptions = {}; + + if (optionsOrForeignKey) { + if (typeof optionsOrForeignKey === 'string') { + options.foreignKey = optionsOrForeignKey; + } else { + options = {...optionsOrForeignKey}; + } + } + return options; +} /** * Stores association meta data for specified class */ export function addAssociation(target: any, - relation: string, - relatedClassGetter: ModelClassGetter, - as: string, - optionsOrForeignKey?: string | ConcatAssociationOptions, - through?: ModelClassGetter | string, - otherKey?: string): void { + association: BaseAssociation): void { let associations = getAssociations(target); - let throughClassGetter; - let options: Partial = {}; if (!associations) { associations = []; } - if (typeof through === 'function') { - throughClassGetter = through; - through = undefined; - } - if (typeof optionsOrForeignKey === 'string') { - options.foreignKey = {name: optionsOrForeignKey}; - } else { - options = {...optionsOrForeignKey}; - } - if (otherKey) { - (options as IAssociationOptionsBelongsToMany).otherKey = {name: otherKey}; - } - - associations.push({ - relation, - relatedClassGetter, - throughClassGetter, - through: through as string, - as, - options - }); + associations.push(association); setAssociations(target, associations); } -/** - * Determines foreign key by specified association (relation) - */ -export function getForeignKey(model: typeof Model, - association: ISequelizeAssociation): string { - const options = association.options as AssociationOptions; - - if (options && options.foreignKey) { - const foreignKey = options.foreignKey; - // if options is an object and has a string foreignKey property, use that as the name - if (typeof foreignKey === 'string') { - return foreignKey; - } - // if options is an object with foreignKey.name, use that as the name - if (foreignKey.name) { - return foreignKey.name; - } - } - - // otherwise calculate the foreign key by related or through class - let classWithForeignKey; - let relatedClass; - - switch (association.relation) { - case BELONGS_TO_MANY: - if (association.throughClassGetter) { - - classWithForeignKey = association.throughClassGetter(); - relatedClass = model; - } else { - throw new Error(`ThroughClassGetter is missing on "${model['name']}"`); - } - break; - case HAS_MANY: - case HAS_ONE: - classWithForeignKey = association.relatedClassGetter(); - relatedClass = model; - break; - case BELONGS_TO: - classWithForeignKey = model; - relatedClass = association.relatedClassGetter(); - break; - default: - } - - const foreignKeys = getForeignKeys(classWithForeignKey.prototype) || []; - - for (const foreignKey of foreignKeys) { - - if (foreignKey.relatedClassGetter() === relatedClass) { - return foreignKey.foreignKey; - } - } - - throw new Error(`Foreign key for "${(relatedClass as any).name}" is missing ` + - `on "${(classWithForeignKey as any).name}".`); -} - /** * Returns association meta data from specified class */ -export function getAssociations(target: any): ISequelizeAssociation[] | undefined { +export function getAssociations(target: any): BaseAssociation[] | undefined { const associations = Reflect.getMetadata(ASSOCIATIONS_KEY, target); if (associations) { return [...associations]; } } -export function setAssociations(target: any, associations: ISequelizeAssociation[]): void { +export function setAssociations(target: any, associations: BaseAssociation[]): void { Reflect.defineMetadata(ASSOCIATIONS_KEY, associations, target); } -export function getAssociationsByRelation(target: any, relatedClass: any): ISequelizeAssociation[] { +export function getAssociationsByRelation(target: any, + relatedClass: any): BaseAssociation[] { const associations = getAssociations(target); return (associations || []).filter(association => { - const _relatedClass = association.relatedClassGetter(); + const _relatedClass = association.getAssociatedClass(); return ( _relatedClass.prototype === relatedClass.prototype || // v3 + v4 /* istanbul ignore next */ @@ -166,77 +88,6 @@ export function addForeignKey(target: any, setForeignKeys(target, foreignKeys); } -/** - * Returns "other" key determined by association object - */ -export function getOtherKey(association: ISequelizeAssociation): string { - const options = association.options as IAssociationOptionsBelongsToMany; - - if (options && options.otherKey) { - const otherKey = options.otherKey; - // if options is an object and has a string otherKey property, use that as the name - if (typeof otherKey === 'string') { - return otherKey; - } - // if options is an object with otherKey.name, use that as the name - if (otherKey.name) { - return otherKey.name; - } - } - return getForeignKey(association.relatedClassGetter(), association); -} - -/** - * Processes association for single model - */ -export function processAssociation(sequelize: BaseSequelize, - model: typeof Model, - association: ISequelizeAssociation): void { - const relatedClass = association.relatedClassGetter(); - const foreignKey = getForeignKey(model, association); - let through; - let otherKey; - - if (association.relation === BELONGS_TO_MANY) { - otherKey = getOtherKey(association); - through = getThroughClass(sequelize, association); - } - - const foreignKeyOptions: Partial = {foreignKey: {name: foreignKey}}; - - if (otherKey) { - foreignKeyOptions.otherKey = {name: otherKey}; - } - - const options = merge( - association.options, - foreignKeyOptions, - { - as: association.as, - through, - } - ); - model[association.relation](relatedClass, options); - - sequelize.adjustAssociation(model, association); -} - -/** - * Returns "through" class determined by association object - */ -export function getThroughClass(sequelize: BaseSequelize, - association: ISequelizeAssociation): any { - if (association.through) { - if (!sequelize.throughMap[association.through]) { - const throughModel = sequelize.getThroughModel(association.through); - sequelize.addModels([throughModel]); - sequelize.throughMap[association.through] = throughModel; - } - return sequelize.throughMap[association.through]; - } - return (association.throughClassGetter as () => typeof Model)(); -} - /** * Returns foreign key meta data from specified class */ diff --git a/lib/services/models.ts b/lib/services/models.ts index 41d5aadd..4e501ec1 100644 --- a/lib/services/models.ts +++ b/lib/services/models.ts @@ -289,7 +289,7 @@ function inferAliasForInclude(include: any, source: any): any { throw new Error(`Alias cannot be inferred: "${source.name}" has multiple ` + `relations with "${include.model.name}"`); } - include.as = associations[0].as; + include.as = associations[0].getSequelizeOptions().as; } } diff --git a/package.json b/package.json index 310fb002..cc09fabd 100644 --- a/package.json +++ b/package.json @@ -1,105 +1,105 @@ { - "name": "sequelize-typescript", - "version": "0.6.0-beta.2", - "description": "Decorators and some other extras for sequelize (v3 + v4)", - "scripts": { - "build": "tsc --project lib/models/v4 && tsc", - "build-tests-es5": "tsc --project test", - "build-tests-es6": "tsc --project test --target es6 && tsc", - "prepare-test-v3": "npm install sequelize@3.30.4 --no-save && npm run build-tests-es5", - "prepare-test-v4": "npm install sequelize@4.22.1 --no-save && npm run build-tests-es6", - "test-v3": "npm run prepare-test-v3 && npm run exec-tests", - "test-v4": "npm run prepare-test-v4 && npm run exec-tests", - "cover-v3": "npm run prepare-test-v3 && nyc --exclude lib/models/v4/**/*.js --all --require source-map-support/register mocha test/specs/", - "cover-v4": "npm run prepare-test-v4 && nyc --exclude lib/models/v3/**/*.js --all --require source-map-support/register mocha test/specs/", - "exec-tests": "mocha test/specs/", - "test": "npm run test-v4 && npm run test-v3", - "cover": "npm run cover-v4 && npm run cover-v3", - "lint": "tslint ." - }, - "repository": { - "type": "git", - "url": "git+https://github.com/RobinBuschmann/sequelize-typescript.git" - }, - "keywords": [ - "orm", - "object relational mapper", - "sequelize", - "typescript", - "decorators", - "mysql", - "sqlite", - "postgresql", - "postgres", - "mssql" + "name": "sequelize-typescript", + "version": "0.6.0-beta.2", + "description": "Decorators and some other extras for sequelize (v3 + v4)", + "scripts": { + "build": "tsc --project lib/models/v4 && tsc", + "build-tests-es5": "tsc --project test --target es5", + "build-tests-es6": "tsc --project test --target es6 && tsc", + "prepare-test-v3": "npm install sequelize@3.30.4 --no-save && npm run build-tests-es5", + "prepare-test-v4": "npm install sequelize@4.22.1 --no-save && npm run build-tests-es6", + "test-v3": "npm run prepare-test-v3 && npm run exec-tests", + "test-v4": "npm run prepare-test-v4 && npm run exec-tests", + "cover-v3": "npm run prepare-test-v3 && nyc --exclude lib/models/v4/**/*.js mocha", + "cover-v4": "npm run prepare-test-v4 && nyc --exclude lib/models/v3/**/*.js mocha", + "exec-tests": "mocha", + "test": "npm run test-v4 && npm run test-v3", + "cover": "npm run cover-v4 && npm run cover-v3", + "lint": "tslint ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RobinBuschmann/sequelize-typescript.git" + }, + "keywords": [ + "orm", + "object relational mapper", + "sequelize", + "typescript", + "decorators", + "mysql", + "sqlite", + "postgresql", + "postgres", + "mssql" + ], + "author": "Robin Buschmann", + "license": "MIT", + "bugs": { + "url": "https://github.com/RobinBuschmann/sequelize-typescript/issues" + }, + "homepage": "https://github.com/RobinBuschmann/sequelize-typescript#readme", + "main": "index.js", + "types": "index.d.ts", + "dependencies": { + "@types/bluebird": "3.5.18", + "@types/node": "6.0.41", + "@types/reflect-metadata": "0.0.4", + "@types/sequelize": "4.0.73", + "es6-shim": "0.35.3" + }, + "devDependencies": { + "@types/chai": "3.4.35", + "@types/chai-as-promised": "0.0.29", + "@types/chai-datetime": "0.0.30", + "@types/lodash": "4.14.54", + "@types/mocha": "2.2.39", + "@types/prettyjson": "0.0.28", + "@types/sinon": "1.16.35", + "@types/sinon-chai": "2.7.27", + "chai": "3.5.0", + "chai-as-promised": "6.0.0", + "chai-datetime": "1.4.1", + "codecov": "2.1.0", + "has-flag": "2.0.0", + "lodash": "4.17.4", + "mocha": "3.2.0", + "moment": "2.17.1", + "mysql": "2.13.0", + "mysql2": "1.3.5", + "nyc": "11.0.2", + "prettyjson": "1.2.1", + "reflect-metadata": "0.1.9", + "sinon": "1.17.7", + "sinon-chai": "2.8.0", + "source-map-support": "0.4.14", + "sqlite3": "3.1.8", + "ts-node": "3.0.4", + "tslint": "4.3.1", + "typescript": "2.5.3", + "uuid-validate": "0.0.2" + }, + "engines": { + "node": ">=0.8.15" + }, + "nyc": { + "lines": 85, + "statements": 85, + "functions": 85, + "branches": 85, + "include": [ + "lib/**/*.js" ], - "author": "Robin Buschmann", - "license": "MIT", - "bugs": { - "url": "https://github.com/RobinBuschmann/sequelize-typescript/issues" - }, - "homepage": "https://github.com/RobinBuschmann/sequelize-typescript#readme", - "main": "index.js", - "types": "index.d.ts", - "dependencies": { - "@types/bluebird": "3.5.18", - "@types/node": "6.0.41", - "@types/reflect-metadata": "0.0.4", - "@types/sequelize": "4.0.73", - "es6-shim": "0.35.3" - }, - "devDependencies": { - "@types/chai": "3.4.35", - "@types/chai-as-promised": "0.0.29", - "@types/chai-datetime": "0.0.30", - "@types/lodash": "4.14.54", - "@types/mocha": "2.2.39", - "@types/prettyjson": "0.0.28", - "@types/sinon": "1.16.35", - "@types/sinon-chai": "2.7.27", - "chai": "3.5.0", - "chai-as-promised": "6.0.0", - "chai-datetime": "1.4.1", - "codecov": "2.1.0", - "has-flag": "2.0.0", - "lodash": "4.17.4", - "mocha": "3.2.0", - "moment": "2.17.1", - "mysql": "2.13.0", - "mysql2": "1.3.5", - "nyc": "11.0.2", - "prettyjson": "1.2.1", - "reflect-metadata": "0.1.9", - "sinon": "1.17.7", - "sinon-chai": "2.8.0", - "source-map-support": "0.4.14", - "sqlite3": "3.1.8", - "ts-node": "3.0.4", - "tslint": "4.3.1", - "typescript": "~2.5.3", - "uuid-validate": "0.0.2" - }, - "engines": { - "node": ">=0.8.15" - }, - "nyc": { - "lines": 85, - "statements": 85, - "functions": 85, - "branches": 85, - "include": [ - "lib/**/*.js" - ], - "exclude": [ - "test/**/*.js" - ], - "reporter": [ - "lcov", - "text-summary" - ], - "cache": true, - "all": true, - "check-coverage": true, - "report-dir": "./coverage" - } + "exclude": [ + "test/**/*.js" + ], + "reporter": [ + "lcov", + "text-summary" + ], + "cache": false, + "all": true, + "check-coverage": true, + "report-dir": "./coverage" + } } diff --git a/test/mocha.opts b/test/mocha.opts index a05fa8cc..76d9a638 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,2 +1,4 @@ +--require source-map-support/register --recursive +test/specs/**/*.js diff --git a/test/models/Hook.ts b/test/models/Hook.ts index 7168c525..67b1a60d 100644 --- a/test/models/Hook.ts +++ b/test/models/Hook.ts @@ -8,6 +8,14 @@ import { AfterSave, AfterUpdate, BeforeSave, BeforeUpdate } from "../../index"; import { BeforeCreate, BeforeValidate, Column, Model, Table } from "../../index"; import { BeforeFind, BeforeFindAfterExpandIncludeAll } from "../../index"; import { BeforeBulkDelete, AfterBulkDelete, AfterDelete, BeforeDelete } from "../../index"; +import {AfterBulkSync} from '../../lib/annotations/hooks/AfterBulkSync'; +import {AfterConnect} from '../../lib/annotations/hooks/AfterConnect'; +import {AfterDefine} from '../../lib/annotations/hooks/AfterDefine'; +import {AfterInit} from '../../lib/annotations/hooks/AfterInit'; +import {BeforeBulkSync} from '../../lib/annotations/hooks/BeforeBulkSync'; +import {BeforeConnect} from '../../lib/annotations/hooks/BeforeConnect'; +import {BeforeDefine} from '../../lib/annotations/hooks/BeforeDefine'; +import {BeforeInit} from '../../lib/annotations/hooks/BeforeInit'; /** * Model used to test hook decorators. Defined hooks are mocked out for testing. @@ -79,6 +87,30 @@ export class Hook extends Model { @AfterBulkCreate static afterBulkCreateHook(instances: Hook[], options: any): void {} + @BeforeBulkSync + static beforeBulkSyncHook(instances: Hook[], options: any): void {} + + @AfterBulkSync + static afterBulkSyncHook(instances: Hook[], options: any): void {} + + @BeforeConnect + static beforeConnectHook(instances: Hook[], options: any): void {} + + @AfterConnect + static afterConnectHook(instances: Hook[], options: any): void {} + + @BeforeDefine + static beforeDefineHook(instances: Hook[], options: any): void {} + + @AfterDefine + static afterDefineHook(instances: Hook[], options: any): void {} + + @BeforeInit + static beforeInitHook(instances: Hook[], options: any): void {} + + @AfterInit + static afterInitHook(instances: Hook[], options: any): void {} + @BeforeBulkDestroy static beforeBulkDestroyHook(options: any): void {} diff --git a/test/specs/annotations/belongs-to-many.spec.ts b/test/specs/annotations/belongs-to-many.spec.ts new file mode 100644 index 00000000..1d13c5e1 --- /dev/null +++ b/test/specs/annotations/belongs-to-many.spec.ts @@ -0,0 +1,37 @@ +import {expect, use} from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import {Table} from '../../../lib/annotations/Table'; +import {Model} from '../../../lib/models/Model'; +import {createSequelize} from '../../utils/sequelize'; +import {BelongsToMany} from '../../../lib/annotations/association/BelongsToMany'; + +use(chaiAsPromised); + +// tslint:disable:max-classes-per-file +describe('BelongsToMany', () => { + + const as = 'manyTeams'; + const sequelize = createSequelize(false); + + @Table + class Team extends Model {} + + @Table + class Player extends Model { + + @BelongsToMany(() => Team, { + as, + through: 'TeamPlayer', + foreignKey: 'playerId', + otherKey: 'teamId', + }) + teams: Team[]; + } + + sequelize.addModels([Team, Player]); + + it('should pass as options to sequelize association', () => { + expect(Player['associations']).to.have.property(as); + }); + +}); diff --git a/test/specs/annotations/belongs-to.spec.ts b/test/specs/annotations/belongs-to.spec.ts new file mode 100644 index 00000000..87f45973 --- /dev/null +++ b/test/specs/annotations/belongs-to.spec.ts @@ -0,0 +1,47 @@ +import {expect, use} from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import {Table} from '../../../lib/annotations/Table'; +import {BelongsTo} from '../../../lib/annotations/association/BelongsTo'; +import {Model} from '../../../lib/models/Model'; +import {createSequelize} from '../../utils/sequelize'; + +use(chaiAsPromised); + +// tslint:disable:max-classes-per-file +describe('BelongsTo', () => { + + const as = 'parent'; + const sequelize = createSequelize(false); + + @Table + class Team extends Model {} + + @Table + class Player extends Model { + + @BelongsTo(() => Team, {as, foreignKey: 'teamId'}) + team: Team; + } + + sequelize.addModels([Team, Player]); + + it('should pass as options to sequelize association', () => { + expect(Player['associations']).to.have.property(as); + }); + + it('should throw due to missing foreignKey', () => { + const _sequelize = createSequelize(false); + + @Table + class Team extends Model {} + + @Table + class Player extends Model { + @BelongsTo(() => Team) + team: Team; + } + + expect(() => _sequelize.addModels([Team, Player])).to.throw(/Foreign key for "\w+" is missing on "\w+"./); + }); + +}); diff --git a/test/specs/annotations/has-many.spec.ts b/test/specs/annotations/has-many.spec.ts new file mode 100644 index 00000000..5e5f744d --- /dev/null +++ b/test/specs/annotations/has-many.spec.ts @@ -0,0 +1,35 @@ +import {expect, use} from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import {Table} from '../../../lib/annotations/Table'; +import {Model} from '../../../lib/models/Model'; +import {createSequelize} from '../../utils/sequelize'; +import {HasMany} from '../../../lib/annotations/association/HasMany'; + +use(chaiAsPromised); + +// tslint:disable:max-classes-per-file +describe('HasMany', () => { + + const as = 'babies'; + const sequelize = createSequelize(false); + + @Table + class Player extends Model {} + + @Table + class Team extends Model { + + @HasMany(() => Player, { + as, + foreignKey: 'teamId' + }) + players: Player[]; + } + + sequelize.addModels([Team, Player]); + + it('should pass as options to sequelize association', () => { + expect(Team['associations']).to.have.property(as); + }); + +}); diff --git a/test/specs/annotations/has-one.spec.ts b/test/specs/annotations/has-one.spec.ts new file mode 100644 index 00000000..58c8a6e9 --- /dev/null +++ b/test/specs/annotations/has-one.spec.ts @@ -0,0 +1,36 @@ +import {expect, use} from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import {Table} from '../../../lib/annotations/Table'; +import {Model} from '../../../lib/models/Model'; +import {createSequelize} from '../../utils/sequelize'; +import {HasOne} from '../../../lib/annotations/association/HasOne'; + +use(chaiAsPromised); + +// tslint:disable:max-classes-per-file +describe('HasOne', () => { + + const as = 'baby'; + const sequelize = createSequelize(false); + + @Table + class Player extends Model {} + + @Table + class Team extends Model { + + @HasOne(() => Player, { + as, + foreignKey: 'teamId' + }) + player: Player; + } + + + sequelize.addModels([Team, Player]); + + it('should pass as options to sequelize association', () => { + expect(Team['associations']).to.have.property(as); + }); + +}); diff --git a/test/specs/association.spec.ts b/test/specs/association.spec.ts index 253208bb..72a48f2d 100644 --- a/test/specs/association.spec.ts +++ b/test/specs/association.spec.ts @@ -12,6 +12,7 @@ import { import {expectAutoGeneratedFunctions} from "../utils/association"; import {assertInstance} from "../utils/common"; import {AllowNull} from "../../lib/annotations/AllowNull"; +import {PrimaryKey} from '../../lib/annotations/PrimaryKey'; use(chaiAsPromised); @@ -22,6 +23,7 @@ class ConcreteModel extends Model {} class BookModel extends ConcreteModel { title: string; pages: PageModel[]; + authors: AuthorModel[]; } class PageModel extends ConcreteModel { content: string; @@ -565,17 +567,19 @@ describe('association', () => { describe('resolve foreign keys automatically', () => { @Table - class Book extends Model { + class Book extends Model implements BookModel { @Column title: string; + authors: any[]; + @HasMany(() => Page) pages: Page[]; } @Table - class Page extends Model { + class Page extends Model implements PageModel { @Column(DataType.TEXT) content: string; @@ -593,17 +597,19 @@ describe('association', () => { describe('set foreign keys explicitly', () => { @Table - class Book2 extends Model { + class Book2 extends Model implements BookModel { @Column title: string; + authors: any[]; + @HasMany(() => Page2, 'bookId') pages: Page2[]; } @Table - class Page2 extends Model { + class Page2 extends Model implements PageModel { @Column(DataType.TEXT) content: string; @@ -617,7 +623,7 @@ describe('association', () => { oneToManyTestSuites(Book2, Page2); }); - function oneToManyWithOptionsTestSuites(Book: typeof ConcreteModel, Page: typeof ConcreteModel, alternateName: boolean = false): void { + function oneToManyWithOptionsTestSuites(Book: typeof BookModel, Page: typeof PageModel, alternateName: boolean = false): void { const foreignKey = alternateName ? 'book_id' : 'bookId'; beforeEach(() => { @@ -670,7 +676,9 @@ describe('association', () => { }; return Page.create(page, {include: [Book]}) - .catch(err => expect(err.message).to.match(new RegExp(`^notNull Violation: (${Page.name}.${foreignKey}|${foreignKey}) cannot be null$`))); + .catch(err => + expect(err.message) + .to.match(new RegExp(`^notNull Violation: (${Page.name}.${foreignKey}|${foreignKey}) cannot be null$`))); }); it('should create instances that require a parent primary key', () => { @@ -699,17 +707,19 @@ describe('association', () => { describe('resolve foreign keys automatically with association options', () => { @Table - class Book3 extends Model { + class Book3 extends Model implements BookModel { @Column title: string; + authors: any[]; + @HasMany(() => Page3, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}) pages: Page3[]; } @Table - class Page3 extends Model { + class Page3 extends Model implements PageModel { @Column(DataType.TEXT) content: string; @@ -727,17 +737,19 @@ describe('association', () => { describe('set foreign keys explicitly with association options', () => { @Table - class Book4 extends Model { + class Book4 extends Model implements BookModel { @Column title: string; + authors: any[]; + @HasMany(() => Page4, {foreignKey: {allowNull: false, name: 'book_id'}, onDelete: 'CASCADE'}) pages: Page4[]; } @Table - class Page4 extends Model { + class Page4 extends Model implements PageModel { @Column(DataType.TEXT) content: string; @@ -755,17 +767,19 @@ describe('association', () => { describe('set foreign keys explicitly via options', () => { @Table - class Book5 extends Model { + class Book5 extends Model implements BookModel { @Column title: string; + authors: any[]; + @HasMany(() => Page5, {foreignKey: 'bookId'}) pages: Page5[]; } @Table - class Page5 extends Model { + class Page5 extends Model implements PageModel { @Column(DataType.TEXT) content: string; @@ -782,9 +796,9 @@ describe('association', () => { describe('Many-to-many', () => { - function manyToManyTestSuites(Book: typeof BookWithAuthorModel, Author: typeof AuthorModel, AuthorBook?: typeof ConcreteModel): void { + function manyToManyTestSuites(Book: typeof BookModel, Author: typeof AuthorModel, AuthorBook?: typeof ConcreteModel): void { - const models : Array = [Book, Author]; + const models: Array = [Book, Author]; if (AuthorBook) { models.push(AuthorBook); @@ -1362,6 +1376,139 @@ describe('association', () => { manyToManyTestSuites(Book3, Author3); }); + + describe('set through model via through options', () => { + + @Table + class Book66 extends Model { + + @Column + title: string; + + pages: any[]; + + @BelongsToMany(() => Author66, { + through: { + model: () => AuthorBook66, + } + }) + authors: Author66[]; + } + + @Table + class AuthorBook66 extends Model { + + @ForeignKey(() => Book66) + bookId: number; + + @ForeignKey(() => Author66) + authorId: number; + } + + @Table + class Author66 extends Model { + + @Column + name: string; + + @BelongsToMany(() => Book66, { + through: { + model: () => AuthorBook66, + } + }) + books: Book66; + } + + manyToManyTestSuites(Book66, Author66, AuthorBook66); + }); + + describe('set through model string via through options', () => { + + @Table + class Book66 extends Model { + + @Column + title: string; + + pages: any[]; + + @BelongsToMany(() => Author66, { + through: { + model: 'AuthorBook66', + }, + foreignKey: 'bookId', + otherKey: 'authorId', + }) + authors: Author66[]; + } + + @Table + class Author66 extends Model { + + @Column + name: string; + + @BelongsToMany(() => Book66, { + through: { + model: 'AuthorBook66', + }, + foreignKey: 'authorId', + otherKey: 'bookId', + }) + books: Book66; + } + + manyToManyTestSuites(Book66, Author66); + }); + + describe('ThroughOptions', () => { + + @Table + class User77 extends Model { + + @Column + name: string; + + @BelongsToMany(() => User77, { + through: { + model: () => Subscription, + scope: { + targetType: 'user' + } + }, + foreignKeyConstraint: true, + foreignKey: 'subscriberId', + otherKey: 'targetId', + constraints: false, + }) + usersSubscribedTo: User77[]; + } + + @Table + class Subscription extends Model { + + @PrimaryKey + @ForeignKey(() => User77) + @Column + subscriberId: number; + + @PrimaryKey + @Column + targetId: number; + + @Column + targetType: string; + } + + sequelize.addModels([User77, Subscription]); + + it('should set scope in pure sequelize association options', () => { + expect(User77['associations'].usersSubscribedTo.through) + .to.have.property('scope').that.eqls({targetType: 'user'}); + }); + + }); + }); describe('One-to-one', () => { @@ -1763,7 +1910,9 @@ describe('association', () => { it('should fail creating instances that require a primary key', () => { return Address.create(petersAddress, {include: [User]}) - .catch(err => expect(err.message).to.match(new RegExp(`^notNull Violation: (${Address.name}.${foreignKey}|${foreignKey}) cannot be null$`))); + .catch(err => + expect(err.message) + .to.match(new RegExp(`^notNull Violation: (${Address.name}.${foreignKey}|${foreignKey}) cannot be null$`))); }); it('should create instances that require a parent primary key', () => { diff --git a/test/specs/hooks/hooks.spec.ts b/test/specs/hooks/hooks.spec.ts index cdd4030a..14814e51 100644 --- a/test/specs/hooks/hooks.spec.ts +++ b/test/specs/hooks/hooks.spec.ts @@ -81,6 +81,15 @@ describe('hook', () => { const afterFindHookStub = sinon.stub(Hook, 'afterFindHook'); const beforeCountHookStub = sinon.stub(Hook, 'beforeCountHook'); + const beforeBulkSyncHookStub = sinon.stub(Hook, 'beforeBulkSyncHook'); + const afterBulkSyncHookStub = sinon.stub(Hook, 'afterBulkSyncHook'); + const beforeConnectHookStub = sinon.stub(Hook, 'beforeConnectHook'); + const afterConnectHookStub = sinon.stub(Hook, 'afterConnectHook'); + const beforeDefineHookStub = sinon.stub(Hook, 'beforeDefineHook'); + const afterDefineHookStub = sinon.stub(Hook, 'afterDefineHook'); + const beforeInitHookStub = sinon.stub(Hook, 'beforeInitHook'); + const afterInitHookStub = sinon.stub(Hook, 'afterInitHook'); + // these hooks are aliases for the equivalent “destroy” hooks const beforeDeleteHookStub = sinon.stub(Hook, 'beforeDeleteHook'); const afterDeleteHookStub = sinon.stub(Hook, 'afterDeleteHook'); @@ -176,6 +185,15 @@ describe('hook', () => { expect(Hook['options'].hooks['afterFind']).to.include(afterFindHookStub); expect(Hook['options'].hooks['beforeCount']).to.include(beforeCountHookStub); + expect(Hook['options'].hooks['beforeBulkSync']).to.include(beforeBulkSyncHookStub); + expect(Hook['options'].hooks['afterBulkSync']).to.include(afterBulkSyncHookStub); + expect(Hook['options'].hooks['beforeConnect']).to.include(beforeConnectHookStub); + expect(Hook['options'].hooks['afterConnect']).to.include(afterConnectHookStub); + expect(Hook['options'].hooks['beforeDefine']).to.include(beforeDefineHookStub); + expect(Hook['options'].hooks['afterDefine']).to.include(afterDefineHookStub); + expect(Hook['options'].hooks['beforeInit']).to.include(beforeInitHookStub); + expect(Hook['options'].hooks['afterInit']).to.include(afterInitHookStub); + expect(Hook['options'].hooks['beforeDestroy']).to.include(beforeDeleteHookStub); expect(Hook['options'].hooks['afterDestroy']).to.include(afterDeleteHookStub); expect(Hook['options'].hooks['beforeBulkDestroy']).to.include(beforeBulkDeleteHookStub); diff --git a/test/specs/services/association.spec.ts b/test/specs/services/association.spec.ts index a32a4c07..8ae83f3a 100644 --- a/test/specs/services/association.spec.ts +++ b/test/specs/services/association.spec.ts @@ -1,74 +1,10 @@ /* tslint:disable:max-classes-per-file */ import {expect} from 'chai'; -import {addAssociation, addForeignKey, getAssociations, getForeignKeys} from "../../../lib/services/association"; +import {addForeignKey, getForeignKeys} from "../../../lib/services/association"; import {Model} from "../../../lib/models/Model"; describe('service.association', () => { - describe('addAssociation', () => { - - it('should add association to target metadata', () => { - const target = {}; - const RELATION = 'hasMany'; - const AS_NAME = 'test'; - const RELATED_CLASS_GETTER = () => class T extends Model {}; - addAssociation(target, RELATION, RELATED_CLASS_GETTER, AS_NAME); - const associations = getAssociations(target); - - expect(associations).to.have.property('length', 1); - expect(associations[0]).to.eql({ - relation: RELATION, - options: {}, - through: undefined, - throughClassGetter: undefined, - as: AS_NAME, - relatedClassGetter: RELATED_CLASS_GETTER, - }); - }); - - it('should add association to target metadata, but not parent', () => { - const parent = {}; - const target = Object.create(parent); - const RELATION = 'hasMany'; - const PARENT_RELATION = 'belongsToMany'; - const AS_NAME = 'test'; - const RELATED_CLASS_GETTER = () => class T extends Model {}; - addAssociation(parent, PARENT_RELATION, RELATED_CLASS_GETTER, AS_NAME); - addAssociation(target, RELATION, RELATED_CLASS_GETTER, AS_NAME); - - const associations = getAssociations(target); - expect(associations).to.have.property('length', 2); - expect(associations[0]).to.eql({ - relation: PARENT_RELATION, - options: {}, - through: undefined, - throughClassGetter: undefined, - as: AS_NAME, - relatedClassGetter: RELATED_CLASS_GETTER, - }); - expect(associations[1]).to.eql({ - relation: RELATION, - options: {}, - through: undefined, - throughClassGetter: undefined, - as: AS_NAME, - relatedClassGetter: RELATED_CLASS_GETTER, - }); - - const parentAssociations = getAssociations(parent); - expect(parentAssociations).to.have.property('length', 1); - expect(parentAssociations[0]).to.eql({ - relation: PARENT_RELATION, - options: {}, - through: undefined, - throughClassGetter: undefined, - as: AS_NAME, - relatedClassGetter: RELATED_CLASS_GETTER, - }); - }); - - }); - describe('addForeignKey', () => { it('should add foreign key to target metadata', () => { diff --git a/test/specs/validation.spec.ts b/test/specs/validation.spec.ts index 56233ba6..b9d7184f 100644 --- a/test/specs/validation.spec.ts +++ b/test/specs/validation.spec.ts @@ -150,8 +150,8 @@ describe('validation', () => { } }; - const validPromises = []; - const invalidPromises = []; + const validPromises: Array> = []; + const invalidPromises: Array> = []; Object .keys(data) diff --git a/test/tsconfig.json b/test/tsconfig.json index 001732c8..f475e9f7 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es6", "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, diff --git a/test/tsconfig.mocha.js b/test/tsconfig.mocha.js new file mode 100644 index 00000000..6094d324 --- /dev/null +++ b/test/tsconfig.mocha.js @@ -0,0 +1,3 @@ +require("ts-node").register({ + project: "test/tsconfig.json", +});