From 298fb8d18df2154c84bea1ade723f7cd46c80602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=A9?= Date: Sat, 15 Jan 2022 16:59:27 +0100 Subject: [PATCH] feat(types): drop TypeScript < 4.1 (#13954) Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com> --- .github/workflows/ci.yml | 276 ++++++------------ docs/manual/other-topics/typescript.md | 378 +++++++++++++++++++++++++ 2 files changed, 467 insertions(+), 187 deletions(-) create mode 100644 docs/manual/other-topics/typescript.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e17aa44685f1..75d39f137eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,144 +1,55 @@ name: CI -on: - push: - branches: - - v7 - pull_request: +on: [push, pull_request] env: SEQ_DB: sequelize_test SEQ_USER: sequelize_test SEQ_PW: sequelize_test -# This configuration cancels previous runs if a new run is started on the same PR. Only one run at a time per PR. -# This does not affect pushes to the v7 branch itself, only PRs. -# from https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-a-fallback-value -concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true - jobs: - install-and-build: - strategy: - fail-fast: false - matrix: - node-version: [14, 16] - name: Upload install and build artifact (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: yarn - - name: Install dependencies and build sequelize - run: yarn install --frozen-lockfile - - name: Compress artifact - run: tar -cf install-build-node-${{ matrix.node-version }}.tar ./lib ./node_modules ./types - - uses: actions/upload-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - path: install-build-node-${{ matrix.node-version }}.tar - retention-days: 1 lint: - name: Lint code + name: Lint code and docs runs-on: ubuntu-latest - needs: install-and-build steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: 16.x - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-16 - - name: Extract artifact - run: tar -xf install-build-node-16.tar - - run: yarn lint-no-fix - unit-test: - strategy: - fail-fast: false - matrix: - node-version: [14, 16] - name: Unit test all dialects (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - needs: lint - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar - - name: Unit tests (mariadb) - run: yarn test-unit-mariadb - - name: Unit tests (mysql) - run: yarn test-unit-mysql - - name: Unit tests (postgres) - run: yarn test-unit-postgres - - name: Unit tests (postgres-native) - run: yarn test-unit-postgres-native - - name: Unit tests (mssql) - run: yarn test-unit-mssql - - name: Unit tests (db2) - run: yarn test-unit-db2 - - name: Unit tests (ibmi) - run: yarn test-unit-ibmi - - name: Unit tests (snowflake) - run: yarn test-unit-snowflake + - run: yarn install --frozen-lockfile + - run: yarn lint + - run: yarn lint-docs docs: - name: Generate TypeDoc + name: Generate docs runs-on: ubuntu-latest - needs: lint steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: 16.x - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-16 - - name: Extract artifact - run: tar -xf install-build-node-16.tar + - run: yarn install --frozen-lockfile - run: yarn docs test-typings: strategy: fail-fast: false matrix: - ts-version: ["4.4", "4.5", "4.6"] + ts-version: ["4.1", "4.2", "4.3", "4.4", "4.5"] name: TS Typings (${{ matrix.ts-version }}) runs-on: ubuntu-latest - needs: lint steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: 16.x - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-16 - - name: Extract artifact - run: tar -xf install-build-node-16.tar - # This step uses npm instead of yarn to minimize the time needed. See #14171 - - name: Install TypeScript - run: npm install --no-save --no-audit typescript@~${{ matrix.ts-version }} - - name: Typing Tests - run: yarn test-typings + - run: yarn install --frozen-lockfile + - run: yarn add --dev typescript@~${{ matrix.ts-version }} + - run: yarn test-typings test-db2: strategy: fail-fast: false matrix: - node-version: [14, 16] + node-version: [10, 16] name: DB2 (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - needs: [ unit-test, test-typings ] env: DIALECT: db2 SEQ_DB: testdb @@ -147,54 +58,48 @@ jobs: SEQ_TEST_CLEANUP_TIMEOUT: 1200000 SEQ_PORT: 50000 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar + - run: yarn install --frozen-lockfile --ignore-engines - name: Install Local DB2 Copy run: yarn start-db2 + - name: Unit Tests + run: yarn test-unit + continue-on-error: true - name: Integration Tests run: yarn test-integration + continue-on-error: true test-sqlite: strategy: fail-fast: false matrix: - node-version: [14, 16] + node-version: [10, 16] name: SQLite (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - needs: [ unit-test, test-typings ] env: DIALECT: sqlite steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar + - run: yarn install --frozen-lockfile --ignore-engines + - name: Unit Tests + run: yarn test-unit - name: Integration Tests run: yarn test-integration test-postgres: strategy: fail-fast: false matrix: - node-version: [14, 16] - postgres-version: [9.5, 10] + node-version: [10, 16] + postgres-version: [9.5, 10] # Does not work with 12 minify-aliases: [true, false] native: [true, false] name: Postgres ${{ matrix.postgres-version }}${{ matrix.native && ' (native)' || '' }} (Node ${{ matrix.node-version }})${{ matrix.minify-aliases && ' (minified aliases)' || '' }} runs-on: ubuntu-latest - needs: [ unit-test, test-typings ] services: postgres: image: sushantdhiman/postgres:${{ matrix.postgres-version }} @@ -211,23 +116,16 @@ jobs: SEQ_PG_MINIFY_ALIASES: ${{ matrix.minify-aliases && '1' || '' }} steps: - run: PGPASSWORD=sequelize_test psql -h localhost -p 5432 -U sequelize_test sequelize_test -c '\l' - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar - - name: Install pg-native (Node 14) - run: yarn add pg-native --ignore-engines - if: matrix.native && matrix.node-version == 14 - # This step uses npm instead of yarn to minimize the time needed. See #14171 - - name: Install pg-native (Node 16) - run: npm install --no-save --no-audit pg-native - if: matrix.native && matrix.node-version == 16 + - run: yarn install --frozen-lockfile --ignore-engines + - run: yarn add pg-native --ignore-engines + if: matrix.native + - name: Unit Tests + run: yarn test-unit + if: ${{ !matrix.minify-aliases }} - name: Integration Tests run: yarn test-integration test-mysql-mariadb: @@ -236,40 +134,39 @@ jobs: matrix: include: - name: MySQL 5.7 - image: mysql:5.7.37 + image: mysql:5.7 dialect: mysql - node-version: 14 + node-version: 10 - name: MySQL 5.7 - image: mysql:5.7.37 + image: mysql:5.7 dialect: mysql node-version: 16 - name: MySQL 8.0 - image: mysql:8.0.28 + image: mysql:8.0 dialect: mysql - node-version: 14 + node-version: 10 - name: MySQL 8.0 - image: mysql:8.0.28 + image: mysql:8.0 dialect: mysql node-version: 16 - name: MariaDB 10.3 - image: mariadb:10.3.34 + image: mariadb:10.3 dialect: mariadb - node-version: 14 + node-version: 10 - name: MariaDB 10.3 - image: mariadb:10.3.34 + image: mariadb:10.3 dialect: mariadb node-version: 16 - name: MariaDB 10.5 image: mariadb:10.5 dialect: mariadb - node-version: 14 + node-version: 10 - name: MariaDB 10.5 image: mariadb:10.5 dialect: mariadb node-version: 16 name: ${{ matrix.name }} (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - needs: [ unit-test, test-typings ] services: mysql: image: ${{ matrix.image }} @@ -286,27 +183,23 @@ jobs: DIALECT: ${{ matrix.dialect }} steps: - run: mysql --host 127.0.0.1 --port 3306 -uroot -psequelize_test -e "GRANT ALL ON *.* TO 'sequelize_test'@'%' with grant option; FLUSH PRIVILEGES;" - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar + - run: yarn install --frozen-lockfile --ignore-engines + - name: Unit Tests + run: yarn test-unit - name: Integration Tests run: yarn test-integration test-mssql: strategy: fail-fast: false matrix: - node-version: [14, 16] + node-version: [10, 16] mssql-version: [2017, 2019] name: MSSQL ${{ matrix.mssql-version }} (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - needs: [ unit-test, test-typings ] services: mssql: image: mcr.microsoft.com/mssql/server:${{ matrix.mssql-version }}-latest @@ -328,52 +221,61 @@ jobs: SEQ_PORT: 1433 steps: - run: /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Password12!" -Q "CREATE DATABASE sequelize_test; ALTER DATABASE sequelize_test SET READ_COMMITTED_SNAPSHOT ON;" - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-${{ matrix.node-version }} - - name: Extract artifact - run: tar -xf install-build-node-${{ matrix.node-version }}.tar + - run: yarn install --frozen-lockfile --ignore-engines + - name: Unit Tests + run: yarn test-unit - name: Integration Tests run: yarn test-integration + test-snowflake: + strategy: + fail-fast: false + matrix: + node-version: [10, 16] + name: SNOWFLAKE (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + env: + DIALECT: snowflake + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: yarn install --frozen-lockfile --ignore-engines + - name: Unit Tests + run: yarn test-unit + # - name: Integration Tests + # run: yarn test-integration release: name: Release runs-on: ubuntu-latest needs: [ + lint, docs, + test-typings, test-sqlite, test-postgres, test-mysql-mariadb, test-mssql, ] - if: github.event_name == 'push' && (github.ref == 'refs/heads/v6' || github.ref == 'refs/heads/v7') + if: github.event_name == 'push' && (github.ref == 'refs/heads/v6' || github.ref == 'refs/heads/v6-beta') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 with: node-version: 16.x - cache: yarn - - uses: actions/download-artifact@v3 - with: - name: install-build-artifact-node-16 - - name: Extract artifact - run: tar -xf install-build-node-16.tar - - run: yarn semantic-release + - run: yarn install --frozen-lockfile + - run: npx semantic-release - id: sequelize uses: sdepold/github-action-get-latest-release@master with: repository: sequelize/sequelize - - name: Notify channels - run: | + - run: | curl -XPOST -u "sdepold:${{ secrets.GH_TOKEN }}" -H "Accept: application/vnd.github.v3+json" -H "Content-Type: application/json" https://api.github.com/repos/sequelize/sequelize/dispatches --data '{"event_type":"Release notifier","client_payload":{"release-id": ${{ steps.sequelize.outputs.id }}}}' - - name: Notify docs repo - run: | - curl -XPOST -u "sdepold:${{ secrets.GH_TOKEN }}" -H "Accept: application/vnd.github.v3+json" -H "Content-Type: application/json" https://api.github.com/repos/sequelize/website/dispatches --data '{"event_type":"Build website"}' diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md new file mode 100644 index 000000000000..684e9a48c286 --- /dev/null +++ b/docs/manual/other-topics/typescript.md @@ -0,0 +1,378 @@ +# TypeScript + +Sequelize provides its own TypeScript definitions. + +Please note that only **TypeScript >= 4.1** is supported. +Our TypeScript support does not follow SemVer. We will support TypeScript releases for at least one year, after which they may be dropped in a SemVer MINOR release. + +As Sequelize heavily relies on runtime property assignments, TypeScript won't be very useful out of the box. A decent amount of manual type declarations are needed to make models workable. + +## Installation + +In order to avoid installation bloat for non TS users, you must install the following typing packages manually: + +- `@types/node` (this is universally required in node projects) +- `@types/validator` + +## Usage + +Example of a minimal TypeScript project with strict type-checking for attributes. + +**Important**: You must use `declare` on your class properties typings to ensure TypeScript does not emit those class properties. +See [Caveat with Public Class Fields](./model-basics.html#caveat-with-public-class-fields) + +**NOTE:** Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly. + +```typescript +/** + * Keep this file in sync with the code in the "Usage" section in typescript.md + */ +import { + Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, + ModelDefined, Optional, Sequelize +} from "sequelize"; + +const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); + +// These are all the attributes in the User model +interface UserAttributes { + id: number; + name: string; + preferredName: string | null; +} + +// Some attributes are optional in `User.build` and `User.create` calls +interface UserCreationAttributes extends Optional {} + +class User extends Model + implements UserAttributes { + declare id: number; // Note that the `null assertion` `!` is required in strict mode. + declare name: string; + declare preferredName: string | null; // for nullable fields + + // timestamps! + declare readonly createdAt: Date; + declare readonly updatedAt: Date; + + // Since TS cannot determine model association at compile time + // we have to declare them here purely virtually + // these will not exist until `Model.init` was called. + declare getProjects: HasManyGetAssociationsMixin; // Note the null assertions! + declare addProject: HasManyAddAssociationMixin; + declare hasProject: HasManyHasAssociationMixin; + declare countProjects: HasManyCountAssociationsMixin; + declare createProject: HasManyCreateAssociationMixin; + + // You can also pre-declare possible inclusions, these will only be populated if you + // actively include a relation. + declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + + declare static associations: { + projects: Association; + }; +} + +interface ProjectAttributes { + id: number; + ownerId: number; + name: string; + description?: string; +} + +interface ProjectCreationAttributes extends Optional {} + +class Project extends Model + implements ProjectAttributes { + declare id: number; + declare ownerId: number; + declare name: string; + + declare readonly createdAt: Date; + declare readonly updatedAt: Date; +} + +interface AddressAttributes { + userId: number; + address: string; +} + +// You can write `extends Model` instead, +// but that will do the exact same thing as below +class Address extends Model implements AddressAttributes { + declare userId: number; + declare address: string; + + declare readonly createdAt: Date; + declare readonly updatedAt: Date; +} + +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +interface NoteCreationAttributes + extends Optional {} + +Project.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + ownerId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + description: { + type: new DataTypes.STRING(128), + allowNull: true, + }, + }, + { + sequelize, + tableName: "projects", + } +); + +User.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + preferredName: { + type: new DataTypes.STRING(128), + allowNull: true, + }, + }, + { + tableName: "users", + sequelize, // passing the `sequelize` instance is required + } +); + +Address.init( + { + userId: { + type: DataTypes.INTEGER.UNSIGNED, + }, + address: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + }, + { + tableName: "address", + sequelize, // passing the `sequelize` instance is required + } +); + +// And with a functional approach defining a module looks like this +const Note: ModelDefined< + NoteAttributes, + NoteCreationAttributes +> = sequelize.define( + "Note", + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + title: { + type: new DataTypes.STRING(64), + defaultValue: "Unnamed Note", + }, + content: { + type: new DataTypes.STRING(4096), + allowNull: false, + }, + }, + { + tableName: "notes", + } +); + +// Here we associate which actually populates out pre-declared `association` static and other methods. +User.hasMany(Project, { + sourceKey: "id", + foreignKey: "ownerId", + as: "projects", // this determines the name in `associations`! +}); + +Address.belongsTo(User, { targetKey: "id" }); +User.hasOne(Address, { sourceKey: "id" }); + +async function doStuffWithUser() { + const newUser = await User.create({ + name: "Johnny", + preferredName: "John", + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const project = await newUser.createProject({ + name: "first!", + ownerId: 123, + }); + + const ourUser = await User.findByPk(1, { + include: [User.associations.projects], + rejectOnEmpty: true, // Specifying true here removes `null` from the return type! + }); + + // Note the `!` null assertion since TS can't know if we included + // the model or not + console.log(ourUser.projects![0].name); +} + +(async () => { + await sequelize.sync(); + await doStuffWithUser(); +})(); +``` + +### Usage without strict types for attributes + +The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it. + +**NOTE:** Keep the following code in sync with `typescriptDocs/ModelInitNoAttributes.ts` to ensure +it typechecks correctly. + +```ts +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 +} + +User.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false, + }, + preferredName: { + type: new DataTypes.STRING(128), + allowNull: true, + }, + }, + { + tableName: "users", + sequelize, // passing the `sequelize` instance is required + } +); + +async function doStuffWithUserModel() { + const newUser = await User.create({ + name: "Johnny", + preferredName: "John", + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const foundUser = await User.findOne({ where: { name: "Johnny" } }); + if (foundUser === null) return; + console.log(foundUser.name); +} +``` + +## Usage of `sequelize.define` + +In Sequelize versions before v5, the default way of defining a model involved using `sequelize.define`. It's still possible to define models with that, and you can also add typings to these models using interfaces. + +**NOTE:** Keep the following code in sync with `typescriptDocs/Define.ts` to ensure +it typechecks correctly. + +```ts +import { Sequelize, Model, DataTypes, Optional } 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; + name: string; +} + +// Some fields are optional when calling UserModel.create() or UserModel.build() +interface UserCreationAttributes extends Optional {} + +// We need to declare an interface for our model that is basically what our class would be +interface UserInstance + extends Model, + UserAttributes {} + +const UserModel = sequelize.define("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); +} +``` + +If you're comfortable with somewhat less strict typing for the attributes on a model, you can save some code by defining the Instance to just extend `Model` without any attributes in the generic types. + +**NOTE:** Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure +it typechecks correctly. + +```ts +import { Sequelize, Model, DataTypes } from "sequelize"; + +const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); + +// We need to declare an interface for our model that is basically what our class would be +interface UserInstance extends Model { + id: number; + name: string; +} + +const UserModel = sequelize.define("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); +} +```