When I cache multiple Sequelize instances while sharing the same models (entities), the last cached instance in memory overrides the Sequelize instance of the models from the previous cache.
for example if i do a request like tenant1.something and the tenant1 will get cached and for the second request tenant2.something and when the second request get cached its overriding the model's instance from the tenant1 cache during the initialization . this issue happen when i was using the class based pattern for my models(entity).
I found the cause of the issue—it happens because of ModelStatic. To resolve this, I switched to using sequelize.define() to define my models.
However, the problem with sequelize.define() is typing is limited (typescript) And the Type ModelCtor is marked for deprecated, currently using modelCtor Instead of modelstatic because the modelstatic returns static instance. which is root cause of this problem
Is there a way to define my entities using the class-based approach with .init() while avoiding issues with shared models?
import {NextFunction, Request, Response} from 'express';
import {getSequelizeInstance} from "../config/connectDB.config";
export const resolveTenantMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
/*
* Browsers do not allow modifying the hostname, hence for development purposes a custom
* header is added (x-host) to simulate a real host under a subdomain (subdomain.example).
*/
const host = process.env.NODE_ENV === "local" ? req.headers["x-host"] as string : req.headers.host;
if (!host) throw new Error("No host provided!");
const parts = host.split('.');
if (parts.length > 2) {
req.domainName = parts[0];
req.isGlobal = false;
} else {
req.domainName = parts[0];
req.isGlobal = true;
}
await getSequelizeInstance(req.domainName, req.isGlobal);
next();
} catch (error) {
console.log(error);
next(error);
}
}
import {Model, ModelStatic, Sequelize} from "sequelize";
import Redis from "ioredis";
import {DBFactory} from "./DBFactory.config";
import {removeUnwantedConstraint} from "../utils/removeUnwantedConstraint.util";
import AppError from "../utils/AppErrors.util";
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
export const sequelizeInstances: { \[key: string\]: Sequelize } = {};
export const modelCache: { \[key: string\]: { \[modelName: string\]: ModelStatic\<Model\<any, any\>\> } } = {};
export let sequelize: Sequelize;
export let globalModels: { \[modelName: string\]: ModelStatic\<Model\<any, any\>\> } = {};
export const getSequelizeInstance = async (domain: string, isGlobal: boolean): Promise\<DBFactory\> =\> {
if (sequelizeInstances\[domain\]) {
sequelize = sequelizeInstances\[domain\];
const factory = new DBFactory(domain, sequelizeInstances[domain], isGlobal);
globalModels = modelCache[domain] || await factory.getModels();
return factory;
}
const cachedConfig = await redis.get(`db_config:${domain}`);
let dbConfig;
if (cachedConfig) {
dbConfig = JSON.parse(cachedConfig);
} else {
dbConfig = {
database: `${domain}`,
username: process.env.DB_USER || "user",
password: process.env.DB_PASSWORD || "1234",
host: process.env.DB_HOST || "localhost",
port: Number(process.env.DB_PORT) || 5432,
};
await redis.set(`db_config:${domain}`, JSON.stringify(dbConfig), "EX", 600);
}
const sequelizeInstance = new Sequelize(dbConfig.database, dbConfig.username, dbConfig.password, {
host: dbConfig.host,
port: dbConfig.port,
dialect: "postgres",
logging: false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000,
},
});
try {
await sequelizeInstance.authenticate();
sequelizeInstances[domain] = sequelizeInstance;
sequelize = sequelizeInstances[domain];
const factory = new DBFactory(domain, sequelizeInstance, isGlobal);
const models = await factory.getModels();
await removeUnwantedConstraint();
globalModels = models;
return factory;
} catch (error: any) {
if (error.parent && error.parent.code === '3D000') {
throw AppError.unauthorized("invalid Domain ")
}
throw error;
}
};
import {Sequelize} from "sequelize";
import {loadModels} from "./loadModels.config";
import {modelCache} from "./connectDB.config";
export class DBFactory {
private domain: string;
private sequelize: Sequelize;
private isGlobal: boolean;
constructor(domain: string, sequelize: Sequelize, isGlobal: boolean) {
this.domain = domain;
this.sequelize = sequelize;
this.isGlobal = isGlobal;
}
async getModels() {
if (modelCache[this.domain]) {
return modelCache[this.domain];
}
const models = await loadModels(this.sequelize, this.isGlobal);
modelCache[this.domain] = models;
return models;
}
async getSequelize() {
return this.sequelize;
}
async transaction(callback: (t: any) => Promise<any>) {
return this.sequelize.transaction(callback);
}
}
import {DataTypes, Model, Optional, Sequelize} from "sequelize";
import {ITenant} from "../../interface/tenantAdmin/IEntityAttributes";
import {TenantStatus} from "../../types/status";
import {Roles} from "../masterEntry/Roles.entity";
import {CompanyInformation} from "./CompanyInformation.entity";
import {TenantAuthentication} from "./TenantAuthentication.entity";
// IN SUPER ADMIN DB
export class Tenant
extends Model\<
ITenant,
Optional\<ITenant, "tenantId" | "createdAt" | "updatedAt" | "deletedAt"\>
\\\>
implements ITenant {
public tenantId!: string;
public tenantName!: string;
public tenantSubDomain!: string;
public emailAddress!: string;
public phoneNumber!: string;
public activeFrom!: Date | null;
public activeTill!: Date | null;
public status!: TenantStatus;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public readonly deletedAt!: Date | null;
static initialize(sequelize: Sequelize) {
Tenant.init(
{
tenantId: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
tenantName: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
len: [1, 255],
},
},
tenantSubDomain: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
validate: {
len: [3, 20],
is: /^[a-z0-9-]+$/,
notIn: [["www", "admin"]],
customValidator(value: string) {
if (value.startsWith('-') || value.endsWith('-')) {
throw new Error("Subdomain cannot start or end with a hyphen");
}
},
},
},
emailAddress: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
phoneNumber: {
type: DataTypes.STRING(15),
allowNull: false,
unique: true,
validate: {
len: [10, 15],
},
},
activeFrom: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
activeTill: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
status: {
type: DataTypes.ENUM(...Object.keys(TenantStatus).map(key => TenantStatus[key as keyof typeof TenantStatus])),
defaultValue: TenantStatus.Draft,
},
createdAt: {type: DataTypes.DATE, defaultValue: DataTypes.NOW},
updatedAt: {type: DataTypes.DATE, defaultValue: DataTypes.NOW},
deletedAt: {type: DataTypes.DATE, allowNull: true},
},
{
sequelize,
tableName: "tenants",
timestamps: true,
paranoid: true,
}
);
}
static associate() {
this.hasOne(CompanyInformation, {foreignKey: "tenantId", onDelete: "CASCADE"});
this.hasOne(TenantAuthentication, {foreignKey: "tenantId", onDelete: "CASCADE"});
Tenant.belongsToMany(Roles, {
through: "TenantRoles",
foreignKey: "tenantId",
otherKey: "roleId",
as: "tenantRoles",
onDelete: "CASCADE",
});
}
}
export default Tenant;
currently using the below pattern but can i define my models class based and is their a way to avoid the data leakage issue
import {DataTypes, Sequelize} from "sequelize";
export const defineTenantModel = async (sequelize: Sequelize) => {
return sequelize.define("Tenant", {
tenantId: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
tenantName: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
len: [1, 255],
},
},
tenantSubDomain: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
validate: {
len: [3, 20],
is: /^[a-z0-9-]+$/,
notIn: [["www", "admin"]],
customValidator(value: string) {
if (value.startsWith('-') || value.endsWith('-')) {
throw new Error("Subdomain cannot start or end with a hyphen");
}
},
},
},
emailAddress: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
phoneNumber: {
type: DataTypes.STRING(15),
allowNull: false,
unique: true,
validate: {
len: [10, 15],
},
},
activeFrom: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
activeTill: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
status: {
type: DataTypes.ENUM("Draft", "Active", "Inactive"),
defaultValue: "Draft",
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
deletedAt: {
type: DataTypes.DATE,
allowNull: true,
},
}, {
tableName: "Tenant",
timestamps: true,
paranoid: true,
});
};
export const associateGlobalTenantModel = async (Tenant: any, CompanyInformation: any, TenantRoles: any, TenantAuthentication: any, Roles: any) => {
await Tenant.hasOne(CompanyInformation, {foreignKey: "tenantId", onDelete: "CASCADE"});
await Tenant.hasOne(TenantAuthentication, {foreignKey: "tenantId", onDelete: "CASCADE"});
await Tenant.belongsToMany(Roles, {
through: TenantRoles,
foreignKey: "tenantId",
otherKey: "roleId",
as: "tenantRoles",
onDelete: "CASCADE",
});
};
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744338974a4569286.html
评论列表(0条)