const GenericDBApi = require('./base.api'); const db = require('../models'); const Utils = require('../utils'); const { applyRuntimeEnvironment, applyRuntimeProjectFilter, } = require('./runtime-context'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; class Project_transition_settingsDBApi extends GenericDBApi { static get MODEL() { return db.project_transition_settings; } static get TABLE_NAME() { return 'project_transition_settings'; } static get SEARCHABLE_FIELDS() { return ['source_key', 'transition_type', 'easing', 'overlay_color']; } static get RANGE_FIELDS() { return ['duration_ms']; } static get ENUM_FIELDS() { return ['environment']; } static get CSV_FIELDS() { return [ 'id', 'environment', 'source_key', 'transition_type', 'duration_ms', 'easing', 'overlay_color', 'createdAt', ]; } static get AUTOCOMPLETE_FIELD() { return 'transition_type'; } static get ASSOCIATIONS() { return [{ field: 'project', setter: 'setProject', isArray: false }]; } static getFieldMapping(data) { // Note: environment and projectId are NOT included here because they are // set explicitly in upsertForProject and should never be changed via data return { id: data.id || undefined, source_key: data.source_key || null, transition_type: data.transition_type || 'fade', duration_ms: data.duration_ms !== undefined ? data.duration_ms : 700, easing: data.easing || 'ease-in-out', overlay_color: data.overlay_color || '#000000', }; } /** * Find settings by project ID and environment. * This is the primary method for fetching transition settings. * * @param {string} projectId - Project ID * @param {string} environment - Environment (dev, stage, production) * @param {object} options - Query options * @returns {object|null} Settings record or null */ static async findByProjectAndEnvironment(projectId, environment, options = {}) { const transaction = options.transaction; const record = await this.MODEL.findOne({ where: { projectId, environment, }, transaction, include: [ { model: db.projects, as: 'project', }, ], }); if (!record) return null; return record.get({ plain: true }); } /** * Create or update settings for a project/environment combination. * Uses upsert semantics - creates if not exists, updates if exists. * * @param {string} projectId - Project ID * @param {string} environment - Environment (dev, stage, production) * @param {object} data - Settings data * @param {object} options - Query options * @returns {object} Created or updated record */ static async upsertForProject(projectId, environment, data, options = {}) { const transaction = options.transaction; const currentUser = options.currentUser; // Check if record exists const existing = await this.MODEL.findOne({ where: { projectId, environment }, transaction, }); if (existing) { // Update existing record await existing.update( { ...this.getFieldMapping(data), updatedById: currentUser?.id || null, }, { transaction }, ); return existing.get({ plain: true }); } // Create new record const newRecord = await this.MODEL.create( { ...this.getFieldMapping(data), projectId, environment, createdById: currentUser?.id || null, updatedById: currentUser?.id || null, }, { transaction }, ); return newRecord.get({ plain: true }); } static async findBy(where, options = {}) { const transaction = options.transaction; const queryWhere = applyRuntimeEnvironment({ ...where }, options); const projectInclude = applyRuntimeProjectFilter( { model: db.projects, as: 'project' }, options, ); const record = await this.MODEL.findOne({ where: queryWhere, transaction, include: [projectInclude], }); if (!record) return null; return record.get({ plain: true }); } static async findAll(filter = {}, options = {}) { filter = filter || {}; const limit = filter.limit || 0; const currentPage = +filter.page || 0; const offset = currentPage * limit; let where = {}; const terms = filter.project ? filter.project.split('|') : []; const validUuids = Utils.filterValidUuids(terms); let include = [ { model: db.projects, as: 'project', where: filter.project ? { [Op.or]: [ ...(validUuids.length > 0 ? [{ id: { [Op.in]: validUuids } }] : []), { name: { [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), }, }, ], } : {}, }, ]; include[0] = applyRuntimeProjectFilter(include[0], options); 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.active !== undefined) { where.active = filter.active === true || filter.active === 'true'; } 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 }; } } where = applyRuntimeEnvironment(where, options); const queryOptions = { where, include, distinct: true, order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], 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; } } } module.exports = Project_transition_settingsDBApi;