feat(pim): align tenant schema for SaaS provisioning
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
const TABLE = "tenants";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const columns = await queryInterface.describeTable(TABLE);
|
||||
|
||||
const additions = [
|
||||
["plan_name", { type: Sequelize.STRING(50), allowNull: false, defaultValue: "STARTER" }],
|
||||
["max_users", { type: Sequelize.INTEGER, allowNull: false, defaultValue: 10 }],
|
||||
["max_products", { type: Sequelize.INTEGER, allowNull: false, defaultValue: 5000 }],
|
||||
["storage_limit_mb", { type: Sequelize.INTEGER, allowNull: false, defaultValue: 5000 }],
|
||||
["subscription_expires_at", { type: Sequelize.DATE, allowNull: true }]
|
||||
];
|
||||
|
||||
for (const [name, definition] of additions) {
|
||||
if (!columns[name]) {
|
||||
await queryInterface.addColumn(TABLE, name, definition);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const columns = await queryInterface.describeTable(TABLE);
|
||||
const removals = [
|
||||
"subscription_expires_at",
|
||||
"storage_limit_mb",
|
||||
"max_products",
|
||||
"max_users",
|
||||
"plan_name"
|
||||
];
|
||||
|
||||
for (const name of removals) {
|
||||
if (columns[name]) {
|
||||
await queryInterface.removeColumn(TABLE, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
for (const table of ["asset_types", "asset_families"]) {
|
||||
const columns = await queryInterface.describeTable(table);
|
||||
if (!columns.tenant_id) {
|
||||
await queryInterface.addColumn(table, "tenant_id", {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: { model: "tenants", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
});
|
||||
}
|
||||
|
||||
await queryInterface.addIndex(table, ["tenant_id"], {
|
||||
name: `${table}_tenant_id_idx`
|
||||
}).catch((error) => {
|
||||
if (!String(error?.message || "").toLowerCase().includes("already exists")) throw error;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
for (const table of ["asset_families", "asset_types"]) {
|
||||
await queryInterface.removeIndex(table, `${table}_tenant_id_idx`).catch(() => {});
|
||||
const columns = await queryInterface.describeTable(table);
|
||||
if (columns.tenant_id) {
|
||||
await queryInterface.removeColumn(table, "tenant_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const definitions = {
|
||||
users: {
|
||||
is_saas_user: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
reset_otp: { type: Sequelize.STRING(10), allowNull: true },
|
||||
reset_otp_expiry: { type: Sequelize.DATE, allowNull: true }
|
||||
},
|
||||
products: {
|
||||
metadata: { type: Sequelize.JSON, allowNull: true, defaultValue: {} }
|
||||
},
|
||||
catalogs: {
|
||||
tenant_id: { type: Sequelize.INTEGER, allowNull: true },
|
||||
attribute_set_id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
references: { model: "attribute_sets", key: "id" },
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
}
|
||||
},
|
||||
categories: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
brands: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
units: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
auditlogs: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
attributes: {
|
||||
tenant_id: { type: Sequelize.INTEGER, allowNull: true },
|
||||
help_text: { type: Sequelize.TEXT, allowNull: true },
|
||||
placeholder: { type: Sequelize.STRING(255), allowNull: true },
|
||||
sortable: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
visible_in_grid: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
|
||||
visible_in_product: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
|
||||
api_visible: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true }
|
||||
},
|
||||
attribute_groups: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
attribute_sets: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
syndication_jobs: {
|
||||
triggered_by: { type: Sequelize.INTEGER, allowNull: true },
|
||||
total_products: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
success_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
failed_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
error_log: { type: Sequelize.JSONB, allowNull: true, defaultValue: [] },
|
||||
started_at: { type: Sequelize.DATE, allowNull: true },
|
||||
completed_at: { type: Sequelize.DATE, allowNull: true }
|
||||
},
|
||||
workflow_registries: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
product_variants: {
|
||||
cost_price: { type: Sequelize.DECIMAL(12, 2), allowNull: true, defaultValue: 0 },
|
||||
currency: { type: Sequelize.STRING(10), allowNull: true, defaultValue: "USD" }
|
||||
},
|
||||
assets: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } },
|
||||
asset_folders: { tenant_id: { type: Sequelize.INTEGER, allowNull: true } }
|
||||
};
|
||||
|
||||
for (const [table, columnsToAdd] of Object.entries(definitions)) {
|
||||
const existing = await queryInterface.describeTable(table);
|
||||
for (const [column, definition] of Object.entries(columnsToAdd)) {
|
||||
if (!existing[column]) {
|
||||
await queryInterface.addColumn(table, column, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tenantScopedTables = [
|
||||
"catalogs", "categories", "brands", "units", "auditlogs", "attributes",
|
||||
"attribute_groups", "attribute_sets", "workflow_registries", "assets", "asset_folders"
|
||||
];
|
||||
for (const table of tenantScopedTables) {
|
||||
const name = `${table}_tenant_id_idx`;
|
||||
const indexes = await queryInterface.showIndex(table);
|
||||
if (!indexes.some((index) => index.name === name)) {
|
||||
await queryInterface.addIndex(table, ["tenant_id"], { name });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const removals = {
|
||||
asset_folders: ["tenant_id"],
|
||||
assets: ["tenant_id"],
|
||||
product_variants: ["currency", "cost_price"],
|
||||
workflow_registries: ["tenant_id"],
|
||||
syndication_jobs: ["completed_at", "started_at", "error_log", "failed_count", "success_count", "total_products", "triggered_by"],
|
||||
attribute_sets: ["tenant_id"],
|
||||
attribute_groups: ["tenant_id"],
|
||||
attributes: ["api_visible", "visible_in_product", "visible_in_grid", "sortable", "placeholder", "help_text", "tenant_id"],
|
||||
auditlogs: ["tenant_id"],
|
||||
units: ["tenant_id"],
|
||||
brands: ["tenant_id"],
|
||||
categories: ["tenant_id"],
|
||||
catalogs: ["attribute_set_id", "tenant_id"],
|
||||
products: ["metadata"],
|
||||
users: ["reset_otp_expiry", "reset_otp", "is_saas_user"]
|
||||
};
|
||||
|
||||
for (const [table, columnsToRemove] of Object.entries(removals)) {
|
||||
const indexName = `${table}_tenant_id_idx`;
|
||||
await queryInterface.removeIndex(table, indexName).catch(() => {});
|
||||
const existing = await queryInterface.describeTable(table);
|
||||
for (const column of columnsToRemove) {
|
||||
if (existing[column]) {
|
||||
await queryInterface.removeColumn(table, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.changeColumn("users", "password_hash", {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const [rows] = await queryInterface.sequelize.query(
|
||||
"SELECT COUNT(*)::int AS count FROM users WHERE password_hash IS NULL"
|
||||
);
|
||||
if (Number(rows?.[0]?.count || 0) > 0) {
|
||||
throw new Error("Cannot restore NOT NULL password_hash while passwordless SaaS users exist");
|
||||
}
|
||||
await queryInterface.changeColumn("users", "password_hash", {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user