fixed projects snapshot issue

This commit is contained in:
Dmitri 2026-03-31 09:56:52 +04:00
parent 3d06d927cf
commit b6cf7a702a
3 changed files with 86 additions and 14 deletions

View File

@ -239,12 +239,26 @@ class Project_element_defaultsDBApi extends GenericDBApi {
return []; return [];
} }
// Dedupe by element_type (keep first occurrence)
// Prevents unique constraint violations if global defaults have duplicates
const seenTypes = new Set();
const dedupedDefaults = globalDefaults.rows.filter((row) => {
if (seenTypes.has(row.element_type)) {
console.warn(
`Duplicate element_type in global defaults: ${row.element_type} (skipping)`,
);
return false;
}
seenTypes.add(row.element_type);
return true;
});
const now = new Date(); const now = new Date();
const currentUserId = options.currentUser?.id || null; const currentUserId = options.currentUser?.id || null;
// Create project defaults from global defaults // Create project defaults from global defaults
const projectDefaults = await this.MODEL.bulkCreate( const projectDefaults = await this.MODEL.bulkCreate(
globalDefaults.rows.map((globalDefault) => ({ dedupedDefaults.map((globalDefault) => ({
projectId, projectId,
element_type: globalDefault.element_type, element_type: globalDefault.element_type,
name: globalDefault.name, name: globalDefault.name,

View File

@ -116,19 +116,12 @@ class ProjectsDBApi extends GenericDBApi {
const project = await super.create(data, options); const project = await super.create(data, options);
// Auto-snapshot global element defaults to the new project // Auto-snapshot global element defaults to the new project
try { // Errors propagate to service layer → transaction rollback → proper error to client
const Project_element_defaultsDBApi = require('./project_element_defaults'); const Project_element_defaultsDBApi = require('./project_element_defaults');
await Project_element_defaultsDBApi.snapshotGlobalDefaults(project.id, { await Project_element_defaultsDBApi.snapshotGlobalDefaults(project.id, {
...options, ...options,
transaction, transaction,
}); });
} catch (error) {
// Log but don't fail project creation if snapshot fails
console.error(
'Failed to snapshot global element defaults to project:',
error,
);
}
return project; return project;
} }

View File

@ -0,0 +1,65 @@
'use strict';
/**
* Remove duplicate element_type_defaults rows.
* Keeps the oldest entry (by createdAt) for each element_type.
* This fixes the unique constraint violation during project creation.
*/
module.exports = {
async up(queryInterface, Sequelize) {
// Find duplicate element_types
const duplicates = await queryInterface.sequelize.query(
`SELECT element_type, COUNT(*) as count
FROM element_type_defaults
WHERE "deletedAt" IS NULL
GROUP BY element_type
HAVING COUNT(*) > 1`,
{ type: Sequelize.QueryTypes.SELECT },
);
if (duplicates.length === 0) {
console.log('No duplicate element_type_defaults found.');
return;
}
console.log(
`Found ${duplicates.length} element_types with duplicates:`,
duplicates.map((d) => d.element_type).join(', '),
);
// For each duplicate element_type, keep oldest and delete others
for (const dup of duplicates) {
// Get all rows for this element_type, ordered by createdAt
const rows = await queryInterface.sequelize.query(
`SELECT id, "createdAt"
FROM element_type_defaults
WHERE element_type = :element_type AND "deletedAt" IS NULL
ORDER BY "createdAt" ASC`,
{
replacements: { element_type: dup.element_type },
type: Sequelize.QueryTypes.SELECT,
},
);
// Keep the first (oldest), delete the rest
const idsToDelete = rows.slice(1).map((r) => r.id);
if (idsToDelete.length > 0) {
await queryInterface.sequelize.query(
`DELETE FROM element_type_defaults WHERE id IN (:ids)`,
{ replacements: { ids: idsToDelete } },
);
console.log(
`Deleted ${idsToDelete.length} duplicate(s) for element_type: ${dup.element_type}`,
);
}
}
console.log('Duplicate removal complete.');
},
async down(_queryInterface, _Sequelize) {
// Cannot restore deleted duplicates
console.log('Down migration not applicable - duplicates cannot be restored.');
},
};