39948-vm/backend/src/db/api/project_element_defaults.js
2026-03-31 12:45:22 +04:00

391 lines
10 KiB
JavaScript

const GenericDBApi = require('./base.api');
const db = require('../models');
const Utils = require('../utils');
const Sequelize = db.Sequelize;
const Op = Sequelize.Op;
class Project_element_defaultsDBApi extends GenericDBApi {
static get MODEL() {
return db.project_element_defaults;
}
static get TABLE_NAME() {
return 'project_element_defaults';
}
static get SEARCHABLE_FIELDS() {
return ['name', 'element_type'];
}
static get RANGE_FIELDS() {
return ['sort_order', 'snapshot_version'];
}
static get ENUM_FIELDS() {
return [];
}
static get ASSOCIATIONS() {
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static get RELATION_FILTERS() {
return [
{
filterKey: 'project',
model: db.projects,
as: 'project',
searchField: 'name',
},
];
}
static get FIND_ALL_INCLUDES() {
return [{ association: 'project' }, { association: 'source_element' }];
}
static get CSV_FIELDS() {
return [
'id',
'element_type',
'name',
'sort_order',
'projectId',
'snapshot_version',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
return 'name';
}
// Declarative field configuration using base class patterns
static get JSON_FIELDS() {
return ['settings_json'];
}
static get FIELD_DEFAULTS() {
return {
element_type: { default: null },
name: { default: null },
sort_order: { default: 0 },
source_element_id: { default: null },
snapshot_version: { default: 1 },
};
}
static getFieldMapping(data) {
// Apply base class transformations (JSON fields, defaults, transformers)
const mapped = super.getFieldMapping(data);
// Custom mapping for projectId field (accepts both projectId and project)
if (mapped.project && !mapped.projectId) {
mapped.projectId = mapped.project;
}
return {
id: mapped.id || undefined,
element_type: mapped.element_type,
name: mapped.name,
sort_order: mapped.sort_order,
settings_json: mapped.settings_json,
source_element_id: mapped.source_element_id,
snapshot_version: mapped.snapshot_version,
projectId: mapped.projectId,
};
}
/**
* Custom findAll with project filtering
* Supports both 'project' and 'projectId' query params for consistency
*/
static async findAll(filter = {}, options = {}) {
filter = filter || {};
const limit = filter.limit || 0;
const currentPage = +filter.page || 0;
const offset = currentPage * limit;
let where = {};
// Support both 'project' and 'projectId' query params
const projectFilter = filter.project || filter.projectId;
const terms = projectFilter ? projectFilter.split('|') : [];
const validUuids = Utils.filterValidUuids(terms);
let include = [
{
model: db.projects,
as: 'project',
where: projectFilter
? {
[Op.or]: [
...(validUuids.length > 0
? [{ id: { [Op.in]: validUuids } }]
: []),
{
name: {
[Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.element_type_defaults,
as: 'source_element',
required: false,
},
];
if (filter.id) {
if (!Utils.isValidUuid(filter.id)) {
return { rows: [], count: 0 };
}
where.id = filter.id;
}
for (const field of this.SEARCHABLE_FIELDS) {
if (filter[field]) {
where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]);
}
}
for (const field of this.RANGE_FIELDS) {
const rangeKey = `${field}Range`;
if (filter[rangeKey]) {
const [start, end] = filter[rangeKey];
if (start !== undefined && start !== null && start !== '') {
where[field] = { ...where[field], [Op.gte]: start };
}
if (end !== undefined && end !== null && end !== '') {
where[field] = { ...where[field], [Op.lte]: end };
}
}
}
for (const field of this.ENUM_FIELDS) {
if (filter[field] !== undefined) {
where[field] = filter[field];
}
}
if (filter.createdAtRange) {
const [start, end] = filter.createdAtRange;
if (start !== undefined && start !== null && start !== '') {
where.createdAt = { ...where.createdAt, [Op.gte]: start };
}
if (end !== undefined && end !== null && end !== '') {
where.createdAt = { ...where.createdAt, [Op.lte]: end };
}
}
const queryOptions = {
where,
include,
distinct: true,
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['sort_order', 'asc']],
transaction: options.transaction,
};
if (!options.countOnly) {
queryOptions.limit = limit ? Number(limit) : undefined;
queryOptions.offset = offset ? Number(offset) : undefined;
}
try {
const { rows, count } = await this.MODEL.findAndCountAll(queryOptions);
return {
rows: options.countOnly ? [] : rows,
count,
};
} catch (error) {
console.error('Error executing query:', error);
throw error;
}
}
/**
* Find project element default by element type for a specific project
*/
static async findByElementType(projectId, elementType, options = {}) {
return this.MODEL.findOne({
where: {
projectId,
element_type: elementType,
deletedAt: null,
},
...options,
});
}
/**
* Snapshot all global element defaults to a project
* Used when creating a new project
*/
static async snapshotGlobalDefaults(projectId, options = {}) {
const Element_type_defaultsDBApi = require('./element_type_defaults');
// Get all global defaults
const globalDefaults = await Element_type_defaultsDBApi.findAll({});
if (!globalDefaults?.rows?.length) {
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 currentUserId = options.currentUser?.id || null;
// Create project defaults from global defaults
const projectDefaults = await this.MODEL.bulkCreate(
dedupedDefaults.map((globalDefault) => ({
projectId,
element_type: globalDefault.element_type,
name: globalDefault.name,
sort_order: globalDefault.sort_order,
settings_json: globalDefault.default_settings_json,
source_element_id: globalDefault.id,
snapshot_version: 1,
createdById: currentUserId,
updatedById: currentUserId,
createdAt: now,
updatedAt: now,
})),
{
transaction: options.transaction,
returning: true,
},
);
return projectDefaults;
}
/**
* Reset a project element default to the current global default
*/
static async resetToGlobal(id, options = {}) {
const Element_type_defaultsDBApi = require('./element_type_defaults');
// Ensure global defaults are initialized
await Element_type_defaultsDBApi.ensureInitialized();
// Find the project default
const projectDefault = await this.MODEL.findByPk(id);
if (!projectDefault) {
throw new Error('Project element default not found');
}
// Find the matching global default
const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({
where: {
element_type: projectDefault.element_type,
deletedAt: null,
},
});
if (!globalDefault) {
throw new Error(
`No global default found for element type: ${projectDefault.element_type}`,
);
}
// Update with global settings and increment version
const now = new Date();
await projectDefault.update(
{
name: globalDefault.name,
sort_order: globalDefault.sort_order,
settings_json: globalDefault.default_settings_json,
source_element_id: globalDefault.id,
snapshot_version: projectDefault.snapshot_version + 1,
updatedById: options.currentUser?.id || null,
updatedAt: now,
},
{
transaction: options.transaction,
},
);
return projectDefault.reload();
}
/**
* Get diff between project default and current global default
*/
static async getDiffFromGlobal(id) {
const Element_type_defaultsDBApi = require('./element_type_defaults');
// Ensure global defaults are initialized
await Element_type_defaultsDBApi.ensureInitialized();
// Find the project default
const projectDefault = await this.MODEL.findByPk(id);
if (!projectDefault) {
throw new Error('Project element default not found');
}
// Find the matching global default
const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({
where: {
element_type: projectDefault.element_type,
deletedAt: null,
},
});
if (!globalDefault) {
return {
projectDefault,
globalDefault: null,
hasGlobalDefault: false,
isDifferent: true,
};
}
// Parse JSON settings for comparison
const projectSettings =
typeof projectDefault.settings_json === 'string'
? JSON.parse(projectDefault.settings_json || '{}')
: projectDefault.settings_json || {};
const globalSettings =
typeof globalDefault.default_settings_json === 'string'
? JSON.parse(globalDefault.default_settings_json || '{}')
: globalDefault.default_settings_json || {};
const isDifferent =
JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) ||
projectDefault.name !== globalDefault.name ||
projectDefault.sort_order !== globalDefault.sort_order;
return {
projectDefault,
globalDefault,
hasGlobalDefault: true,
isDifferent,
projectSettings,
globalSettings,
};
}
}
module.exports = Project_element_defaultsDBApi;