diff --git a/dev/create-sequelize-instance.ts b/dev/create-sequelize-instance.ts index 1ec6d6438..72153aba2 100644 --- a/dev/create-sequelize-instance.ts +++ b/dev/create-sequelize-instance.ts @@ -1,15 +1,21 @@ -import type { Options as Sequelize6Options } from 'sequelize'; -import { Sequelize as Sequelize6 } from 'sequelize'; -import type { Options as Sequelize7Options, Sequelize as Sequelize7 } from '@sequelize/core'; -import { wrapOptions } from './wrap-options'; +import type { Options as Sequelize6Options } from "sequelize"; +import { Sequelize as Sequelize6 } from "sequelize"; +import type { + Options as Sequelize7Options, + Sequelize as Sequelize7, +} from "@sequelize/core"; +import { wrapOptions, wrapOptionsV7 } from "./wrap-options"; -export function createSequelize6Instance(options?: Sequelize6Options): Sequelize6 { +export function createSequelize6Instance( + options?: Sequelize6Options +): Sequelize6 { return new Sequelize6(wrapOptions(options)); } -export function createSequelize7Instance(options?: Sequelize7Options): Sequelize7 { +export function createSequelize7Instance( + options?: Sequelize7Options +): Sequelize7 { // not compatible with node 10 - const { Sequelize: Sequelize7Constructor } = require('@sequelize/core'); - // @ts-expect-error -- wrapOptions expect sequelize 6. - return new Sequelize7Constructor(wrapOptions(options)); + const { Sequelize: Sequelize7Constructor } = require("@sequelize/core"); + return new Sequelize7Constructor(wrapOptionsV7(options)); } diff --git a/dev/wrap-options.ts b/dev/wrap-options.ts index e0f950acb..6e958fdb6 100644 --- a/dev/wrap-options.ts +++ b/dev/wrap-options.ts @@ -1,15 +1,24 @@ -import defaults from 'lodash/defaults.js'; -import { CiDbConfigs } from './ci-db-configs'; -import { log } from './logging'; -import type { Dialect, Options } from 'sequelize'; +import defaults from "lodash/defaults.js"; +import { CiDbConfigs } from "./ci-db-configs"; +import { log } from "./logging"; +import type { + Dialect as Sequelize6Dialect, + Options as Sequelize6Options, +} from "sequelize"; +import type { + Options as Sequelize7Options, + Sequelize as Sequelize7, +} from "@sequelize/core"; -export function wrapOptions(options: Options = {}) { +export function wrapOptions(options: Sequelize6Options = {}) { if (!process.env.DIALECT) { - throw new Error('Dialect is not defined! Aborting.'); + throw new Error("Dialect is not defined! Aborting."); } - const isPostgresNative = process.env.DIALECT === 'postgres-native'; - const dialect = (isPostgresNative ? 'postgres' : process.env.DIALECT) as Dialect; + const isPostgresNative = process.env.DIALECT === "postgres-native"; + const dialect = ( + isPostgresNative ? "postgres" : process.env.DIALECT + ) as Sequelize6Dialect; // this fails in the CI due to mismatch between Sequelize 6 & 7. Should be resolved once we drop Sequelize 6. // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error @@ -31,3 +40,35 @@ export function wrapOptions(options: Options = {}) { return options; } + +export function wrapOptionsV7(options: Partial> = {}) { + if (!process.env.DIALECT) { + throw new Error("Dialect is not defined! Aborting."); + } + + const isPostgresNative = process.env.DIALECT === "postgres-native"; + const dialect = isPostgresNative ? "postgres" : process.env.DIALECT; + + // Get the CI config for this dialect + const config = CiDbConfigs[dialect as keyof typeof CiDbConfigs] as any; + + // Transform Sequelize v6-style options to v7-style + const transformedConfig = { ...config }; + if (transformedConfig.username) { + transformedConfig.user = transformedConfig.username; + delete transformedConfig.username; + } + + const finalOptions = { + dialect: dialect, + logging: log, + ...transformedConfig, + ...options, + }; + + if (isPostgresNative) { + finalOptions.native = true; + } + + return finalOptions; +} diff --git a/src/sscce-sequelize-7.ts b/src/sscce-sequelize-7.ts index 603cb219c..80f9fc057 100644 --- a/src/sscce-sequelize-7.ts +++ b/src/sscce-sequelize-7.ts @@ -1,43 +1,389 @@ -import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from '@sequelize/core'; -import { Attribute, NotNull } from '@sequelize/core/decorators-legacy'; -import { createSequelize7Instance } from '../dev/create-sequelize-instance'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Options, +} from "@sequelize/core"; +import { createSequelize7Instance } from "../dev/create-sequelize-instance"; +import { expect } from "chai"; +import { PostgresDialect } from "@sequelize/postgres"; +import { + Attribute, + AutoIncrement, + BelongsToMany, + HasMany, + Index, + NotNull, + PrimaryKey, +} from "@sequelize/core/decorators-legacy"; // if your issue is dialect specific, remove the dialects you don't need to test on. -export const testingOnDialects = new Set(['mssql', 'sqlite', 'mysql', 'mariadb', 'postgres', 'postgres-native']); +export const testingOnDialects = new Set(["postgres"]); -// You can delete this file if you don't want your SSCCE to be tested against Sequelize 7 +class Location extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @Attribute(DataTypes.TEXT) + declare name?: string; + + @HasMany(() => System, { + foreignKey: "locationId", + inverse: "location", + }) + declare systems?: NonAttribute; + + @BelongsToMany(() => Customer, { + through: { + model: () => CustomerLocation, + // Only "active" relationships + scope: { endAt: null }, + }, + foreignKey: "locationId", + otherKey: "customerId", + inverse: { + as: "locations", + }, + }) + declare customers?: NonAttribute; +} + +class Customer extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @Attribute(DataTypes.TEXT) + declare name?: CreationOptional; + + declare locations?: NonAttribute; + declare CustomerLocation?: NonAttribute; +} + +class System extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @Attribute(DataTypes.TEXT) + declare name: string; + + @Attribute(DataTypes.INTEGER) + declare locationId: number; + declare location?: Location; + + @HasMany(() => FuelDelivery, { + foreignKey: "systemId", + inverse: "system", + }) + declare fuelDeliveries?: FuelDelivery[]; +} + +class FuelDelivery extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.INTEGER) + @AutoIncrement + @PrimaryKey + declare id: CreationOptional; + + @Attribute(DataTypes.TEXT) + declare product: string; + + @Attribute(DataTypes.INTEGER) + declare systemId: number; + + declare system?: System; +} + +class CustomerLocation extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.INTEGER) + @PrimaryKey + @NotNull + declare customerId: number; + + @Attribute(DataTypes.INTEGER) + @PrimaryKey + @NotNull + declare locationId: number; + + @Attribute(DataTypes.TEXT) + declare relationType: string; + + @Attribute(DataTypes.DATE(6)) + @Index() + declare endAt?: Date | null; + + declare customer?: Customer; + + declare location?: Location; +} // Your SSCCE goes inside this function. export async function run() { // This function should be used instead of `new Sequelize()`. // It applies the config for your SSCCE to work on CI. const sequelize = createSequelize7Instance({ - logQueryParameters: true, - benchmark: true, + minifyAliases: true, + dialect: "postgres", define: { - // For less clutter in the SSCCE + // Keep model definitions lean so the regression focus stays on include resolution. timestamps: false, }, - }); + models: [Customer, Location, System, FuelDelivery, CustomerLocation], + } as Options); - class Foo extends Model, InferCreationAttributes> { - declare id: CreationOptional; + try { + await sequelize.sync({ force: true }); - @Attribute(DataTypes.TEXT) - @NotNull - declare name: string; - } + const customer = await Customer.create({ name: "Propane Delivery Co" }); + const customer2 = await Customer.create({ name: "Kozy Operations Inc" }); + const location = await Location.create({ name: "Fuel Depot" }); + await CustomerLocation.create({ + customerId: customer.id, + locationId: location.id, + relationType: "primary", + }); + + await CustomerLocation.create({ + customerId: customer2.id, + locationId: location.id, + relationType: "secondary", + }); + + const system = await System.create({ + name: "Kozy Operations Inc", + locationId: location.id, + }); + + console.log(system); + const delivery = await FuelDelivery.create({ + product: "Propane", + systemId: system.id, + }); - sequelize.addModels([Foo]); + const result = await FuelDelivery.findByPk(delivery.id, { + logging: console.log, + include: [ + { + association: "system", + required: true, + include: [ + { + association: "location", + required: true, + include: [ + { + association: "customers", + required: true, + }, + ], + }, + ], + }, + ], + }); - // You can use sinon and chai assertions directly in your SSCCE. - const spy = sinon.spy(); - sequelize.afterBulkSync(() => spy()); - await sequelize.sync({ force: true }); - expect(spy).to.have.been.called; + expect(result).to.not.be.null; + expect(result!.system).to.not.be.undefined; + expect(result!.system!.location).to.not.be.undefined; + const customers = result!.system!.location!.customers; + expect(customers).to.not.be.undefined; + expect(customers).to.have.length(2); + expect(customers![0].id).to.equal(customer.id); + expect(customers![1].id).to.equal(customer2.id); - console.log(await Foo.create({ name: 'TS foo' })); - expect(await Foo.count()).to.equal(1); + // Test Two + + const result2 = await Customer.findOne({ + include: [ + { + association: "locations", + + include: [ + { + association: "customers", + }, + ], + }, + ], + }); + + expect(result2).to.not.be.null; + expect(result2!.locations).to.not.be.undefined; + const locations = result2!.locations!; + expect(locations).to.have.length.greaterThan(0); + expect(locations[0].customers).to.not.be.undefined; + expect(locations[0].customers).to.have.length.greaterThan(0); + + const result3 = await FuelDelivery.findByPk(delivery.id, { + include: [ + { + association: "system", + include: [ + { + association: "location", + include: [ + { + association: "customers", + required: true, + include: [ + { + association: "locations", + required: false, + include: [ + { + association: "systems", + where: { name: "Kozy Operations Inc" }, + required: true, + include: [ + { + association: "location", + include: [ + { + association: "customers", + required: false, + include: [ + { + association: "locations", + required: false, + include: [ + { + association: "systems", + where: { + name: "Kozy Operations Inc", + }, + required: false, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(result3).to.not.be.null; + expect(result3!.system).to.not.be.undefined; + expect(result3!.system!.location).to.not.be.undefined; + + const customers3 = result3!.system!.location!.customers; + expect(customers3).to.not.be.undefined; + expect(customers3).to.have.length(2); + expect(customers3![0].id).to.equal(customer.id); + expect(customers3![0].locations).to.not.be.undefined; + expect(customers3![0].locations).to.have.length.greaterThan(0); + expect(customers3![0].locations![0].systems).to.not.be.undefined; + expect(customers3![0].locations![0].systems).to.have.length.greaterThan(0); + expect(customers3![0].locations![0].systems![0].name).to.equal( + "Kozy Operations Inc" + ); + + const result4 = await FuelDelivery.findByPk(delivery.id, { + include: [ + { + association: "system", + include: [ + { + association: "location", + include: [ + { + association: "customers", + required: true, + include: [ + { + association: "locations", + required: false, + include: [ + { + association: "systems", + where: { name: "Kozy Operations Inc" }, + required: true, + include: [ + { + association: "location", + include: [ + { + association: "customers", + required: false, + include: [ + { + association: "locations", + required: true, + include: [ + { + association: "systems", + where: { + name: "Kozy Operations Inc", + }, + required: false, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(result4).to.not.be.null; + expect(result4!.system).to.not.be.undefined; + expect(result4!.system!.location).to.not.be.undefined; + + const customers4 = result4!.system!.location!.customers; + expect(customers4).to.not.be.undefined; + expect(customers4).to.have.length(2); + expect(customers4![0].id).to.equal(customer.id); + expect(customers4![0].locations).to.not.be.undefined; + expect(customers4![0].locations).to.have.length.greaterThan(0); + expect(customers4![0].locations![0].systems).to.not.be.undefined; + expect(customers4![0].locations![0].systems).to.have.length.greaterThan(0); + expect(customers4![0].locations![0].systems![0].name).to.equal( + "Kozy Operations Inc" + ); + } finally { + await sequelize.close(); + } }