Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork4.3k
Description
This is a long one, bear with me.
I'd like to bring built-in support for decorator based model declaration in Sequelize. It's one of the main arguments I've seen in favor of using TypeORM over the years and I think it would help reduce the boilerplate of creating a new model.
I am not going to lie, a lot of this is very inspired bysequelize-typescript
, with some differences.
Prior art
Foreword: Legacy Decorators & Stage 2 Decorators
Both the TypeScript & Babel communities have been using Decorators for years. These decorators follow the old decorator spec.A new spec has been in the works for a few years now. It is unclear when stable decorators will actually land in ECMAScript.
As such, I propose to do the initial implementation using Legacy Decorators but expose them throughsequelize/decorators-legacy
. Once decorators become stage 4, we can do a parallel implementation that is directly exposed in the rootsequelize
import, and deprecate/decorators-legacy
.
API Design
Model registration
One of the first parts of the design would be to provide a way to register a model that has been decorated. A basic building block that's decorator-implementation agnostic. Both for existing third-party packages and a future stage-4 decorators implementation.
I would expose two methods:registerModelAttributeOptions
®isterModelOptions
:
/** * Registers attribute options for future registering of the Model using Model.init * Subsequent calls for the same model & attributeName will be merged, with the newer call taking precedence. */registerModelAttributeOptions(model:typeofModel,attributeName: string,options:Partial<ModelAttributeColumnOptions>):void;/** * Registers model options for future registering of the Model using Model.init * Subsequent calls for the same model & attributeName will be merged, with the newer call taking precedence. * 'sequelize' option is not accepted here. Pass it through `Model.init` when registering the model. */registerModelOptions(model:typeofModel,options:Partial<ModelOptions>):void;
We then also need a way to register the model to Sequelize. I see a few options:
- Overload
Model.init
:classModel{// use this one when registering a decorated model.staticinit(options:{sequelize:Sequelize});// use this one when registering a non-decorated model.staticinit(attributes:ModelAttributes,options:InitOptions);}
- Overload
Sequelize#define
- Add
Sequelize#registerModels
- Add a
models
parameter to the Sequelize constructor.
I'd opt for overloading.
Automatic model registration
Similarly to what TypeORM & SequelizeTypescript are doing, we could add an async methodSequelize#importModels(glob)
that loads files matching the glob (using ESM dynamic import), and register any export that extends model and isn't tagged asabstract
(see Model Inheritance).
Model Inheritance
this would resolve#1243
A big benefit of decorator-based definition is that it becomes possible to inherit definitions (both model options & attributes):
@ModelOptions({underscored:true,abstract:true,// do not actually register this model!})abstractclassBaseModelextendsModel{ @Column(DataType.TEXT,{unique:true,defaultValue:shortId(),field:'external_id'})publicId:string; @Column(DataType.INTEGER,{primaryKey:true,autoIncrement:true,field:'private_id'})privateId:number;}// inherits 'publicId' & 'privateId' from BaseModelclassUserextendsMyBaseModel{ @Column(DataType.TEXT)name:string;}// inherits 'publicId' & 'privateId' from BaseModelclassProjectextendsMyBaseModel{ @Column(DataType.TEXT)name:string;}
Model Options Decorator:@ModelOptions
Note: Name is already taken by typing. Alternative names:@Model
(already taken),@Table
,@Entity
, etc...
The simplest design for a Model Option decorator would be one that simply accepts the model options:
// abstract is for the creation of base models, see "inheritance"// the `abstract` tag is of course not itself inherited :)functionModelOptions(options:TModelOptions&{abstract?:boolean});@ModelOptions({tableName:'users'})classUserextendsMyBaseModel{}
Model Attribute Decorator
We have two choices here: A simpleAttribute
decorator which acceptsModelAttributeColumnOptions
, or a decorator per-option like withsequelize-typescript.
This is the design I came up with, critics and counter-proposals welcome:
typeTimestampAttributeOptions=Omit<ModelAttributeColumnOptions,'type'|'allowNull'|'unique'|'primaryKey'|'autoIncrement'|'defaultValue'|'autoIncrementIdentity'|'references'|'onUpdate'|'onDelete'>;// use one of these to define an attribute:functionCreatedAtAttribute(options?:TimestampAttributeOptions);functionUpdateAtAttribute(options?:TimestampAttributeOptions);functionDeletedAtAttribute(options?:TimestampAttributeOptions);functionAttribute(type:DataType,options?:Omit<ModelAttributeColumnOptions,'type'>);
Model Association Decorator
This part depends on RFC#14302 and is described over there but is basically: a decorator per association type:
import{Model,HasManyAttribute,HasMany}from"sequelize";classUserextendsModel{ @HasMany(Project)readonlyprojects!:HasManyAttribute<Project,number>;}classUserextendsModel{ @BelongsTo(User,{foreignKey:'authorId',onDelete:'CASCADE',})readonlyauthor!:BelongsToAttribute<User,number>; @Attribute(DataType.number,{allowNull:false})authorId:number;}
emitDecoratorMetadata
I would vote against guessing whichDataType
to use based on decorator metadata. Having defaults forstring
,number
, etc.. will lead to users accidentally using the Column Type as they're not strict enough.
other elements to consider
- decorators for hooks
- decorators for scopes
- decorators for validators
Metadata
Metadata
Assignees
Labels
Type
Projects
Status