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;