Skip to content

Commit

Permalink
feat(types): make Model.init aware of pre-configured foreign keys (s…
Browse files Browse the repository at this point in the history
  • Loading branch information
ephys authored and aliatsis committed Jun 2, 2022
1 parent c420666 commit 9e21bac
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 182 deletions.
37 changes: 32 additions & 5 deletions src/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ValidationOptions } from './instance-validator';
import { IndexesOptions, QueryOptions, TableName } from './dialects/abstract/query-interface';
import { Sequelize, SyncOptions } from './sequelize';
import { Col, Fn, Literal, Where, MakeNullishOptional, AnyFunction, Cast, Json } from './utils';
import { LOCK, Transaction, Op } from './index';
import { LOCK, Transaction, Op, Optional } from './index';
import { SetRequired } from './utils/set-required';

export interface Logging {
Expand Down Expand Up @@ -1962,7 +1962,12 @@ export abstract class Model<TModelAttributes extends {} = any, TCreationAttribut
*/
public static init<MS extends ModelStatic<Model>, M extends InstanceType<MS>>(
this: MS,
attributes: ModelAttributes<M, Attributes<M>>, options: InitOptions<M>
attributes: ModelAttributes<
M,
// 'foreign keys' are optional in Model.init as they are added by association declaration methods
Optional<Attributes<M>, BrandedKeysOf<Attributes<M>, typeof ForeignKeyBrand>>
>,
options: InitOptions<M>
): MS;

/**
Expand Down Expand Up @@ -3272,6 +3277,10 @@ type IsBranded<T, Brand extends symbol> = keyof NonNullable<T> extends keyof Omi
? false
: true;

type BrandedKeysOf<T, Brand extends symbol> = {
[P in keyof T]-?: IsBranded<T[P], Brand> extends true ? P : never
}[keyof T];

/**
* Dummy Symbol used as branding by {@link NonAttribute}.
*
Expand All @@ -3284,14 +3293,32 @@ declare const NonAttributeBrand: unique symbol;
* You can use it to tag fields from your class that are NOT attributes.
* They will be ignored by {@link InferAttributes} and {@link InferCreationAttributes}
*/

export type NonAttribute<T> =
// we don't brand null & undefined as they can't have properties.
// This means `NonAttribute<null>` will not work, but who makes an attribute that only accepts null?
// Note that `NonAttribute<string | null>` does work!
T extends null | undefined ? T
: (T & { [NonAttributeBrand]?: true });

/**
* Dummy Symbol used as branding by {@link ForeignKey}.
*
* Do not export, Do not use.
*/
declare const ForeignKeyBrand: unique symbol;

/**
* This is a Branded Type.
* You can use it to tag fields from your class that are foreign keys.
* They will become optional in {@link Model.init} (as foreign keys are added by association methods, like {@link Model.hasMany}.
*/
export type ForeignKey<T> =
// we don't brand null & undefined as they can't have properties.
// This means `ForeignKey<null>` will not work, but who makes an attribute that only accepts null?
// Note that `ForeignKey<string | null>` does work!
T extends null | undefined ? T
: (T & { [ForeignKeyBrand]?: true });

/**
* Option bag for {@link InferAttributes}.
*
Expand Down Expand Up @@ -3366,8 +3393,8 @@ declare const CreationAttributeBrand: unique symbol;
*/
export type CreationOptional<T> =
// we don't brand null & undefined as they can't have properties.
// This means `CreationAttributeBrand<null>` will not work, but who makes an attribute that only accepts null?
// Note that `CreationAttributeBrand<string | null>` does work!
// This means `CreationOptional<null>` will not work, but who makes an attribute that only accepts null?
// Note that `CreationOptional<string | null>` does work!
T extends null | undefined ? T
: (T & { [CreationAttributeBrand]?: true });

Expand Down
13 changes: 13 additions & 0 deletions test/types/infer-attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
Attributes,
CreationAttributes,
CreationOptional,
DataTypes,
ForeignKey,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from 'sequelize';

class Project extends Model<InferAttributes<Project>> {
Expand All @@ -32,6 +35,7 @@ class User extends Model<InferAttributes<User, { omit: 'omittedAttribute' | 'omi
declare omittedAttributeArray: number[];

declare joinedEntity?: NonAttribute<Project>;
declare projectId: CreationOptional<ForeignKey<number>>;

instanceMethod() {
}
Expand All @@ -40,6 +44,15 @@ class User extends Model<InferAttributes<User, { omit: 'omittedAttribute' | 'omi
}
}

User.init({
mandatoryArrayAttribute: DataTypes.ARRAY(DataTypes.STRING),
mandatoryAttribute: DataTypes.STRING,
// projectId is omitted but still works, because it is branded with 'ForeignKey'
nullableOptionalAttribute: DataTypes.STRING,
optionalArrayAttribute: DataTypes.ARRAY(DataTypes.STRING),
optionalAttribute: DataTypes.INTEGER,
}, { sequelize: new Sequelize() });

type UserAttributes = Attributes<User>;
type UserCreationAttributes = CreationAttributes<User>;

Expand Down
21 changes: 8 additions & 13 deletions test/types/typescriptDocs/Define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,32 @@
*
* Don't include this comment in the md file.
*/
import { Sequelize, Model, DataTypes, Optional } from 'sequelize';
import { Sequelize, Model, DataTypes, CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize';

const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb');

// We recommend you declare an interface for the attributes, for stricter typechecking
interface UserAttributes {
id: number;

interface UserModel extends Model<InferAttributes<UserModel>, InferCreationAttributes<UserModel>> {
// Some fields are optional when calling UserModel.create() or UserModel.build()
id: CreationOptional<number>;
name: string;
}

// Some fields are optional when calling UserModel.create() or UserModel.build()
interface UserCreationAttributes extends Optional<UserAttributes, 'id'> {}

// We need to declare an interface for our model that is basically what our class would be
interface UserInstance
extends Model<UserAttributes, UserCreationAttributes>,
UserAttributes {}

const UserModel = sequelize.define<UserInstance>('User', {
const UserModel = sequelize.define<UserModel>('User', {
id: {
primaryKey: true,
type: DataTypes.INTEGER.UNSIGNED,
},
name: {
type: DataTypes.STRING,
}
},
});

async function doStuff() {
const instance = await UserModel.findByPk(1, {
rejectOnEmpty: true,
});

console.log(instance.id);
}
32 changes: 0 additions & 32 deletions test/types/typescriptDocs/DefineNoAttributes.ts

This file was deleted.

17 changes: 7 additions & 10 deletions test/types/typescriptDocs/ModelInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin,
HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin,
HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional,
Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute
Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ForeignKey,
} from 'sequelize';

const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb');
Expand Down Expand Up @@ -62,7 +62,11 @@ class Project extends Model<
> {
// id can be undefined during creation when using `autoIncrement`
declare id: CreationOptional<number>;
declare ownerId: number;

// foreign keys are automatically added by associations methods (like Project.belongsTo)
// by branding them using the `ForeignKey` type, `Project.init` will know it does not need to
// display an error if ownerId is missing.
declare ownerId: ForeignKey<User['id']>;
declare name: string;

// `owner` is an eagerly-loaded association.
Expand All @@ -79,7 +83,7 @@ class Address extends Model<
InferAttributes<Address>,
InferCreationAttributes<Address>
> {
declare userId: number;
declare userId: ForeignKey<User['id']>;
declare address: string;

// createdAt can be undefined during creation
Expand All @@ -95,10 +99,6 @@ Project.init(
autoIncrement: true,
primaryKey: true
},
ownerId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false
},
name: {
type: new DataTypes.STRING(128),
allowNull: false
Expand Down Expand Up @@ -138,9 +138,6 @@ User.init(

Address.init(
{
userId: {
type: DataTypes.INTEGER.UNSIGNED
},
address: {
type: new DataTypes.STRING(128),
allowNull: false
Expand Down
6 changes: 3 additions & 3 deletions test/types/typescriptDocs/ModelInitNoAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { Sequelize, Model, DataTypes } from 'sequelize';
const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb');

class User extends Model {
public id!: number; // Note that the `null assertion` `!` is required in strict mode.
public name!: string;
public preferredName!: string | null; // for nullable fields
declare id: number;
declare name: string;
declare preferredName: string | null;
}

User.init(
Expand Down

0 comments on commit 9e21bac

Please sign in to comment.