const GenericDBApi = require('./base.api'); const db = require('../models'); const Utils = require('../utils'); const { getRuntimeProjectSlug } = require('./runtime-context'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; class ProjectsDBApi extends GenericDBApi { static get MODEL() { return db.projects; } static get TABLE_NAME() { return 'projects'; } static get SEARCHABLE_FIELDS() { return [ 'name', 'slug', 'description', 'logo_url', 'favicon_url', 'og_image_url', ]; } static get RANGE_FIELDS() { return []; } static get ENUM_FIELDS() { return []; } static get CSV_FIELDS() { return [ 'id', 'name', 'slug', 'description', 'logo_url', 'createdAt', ]; } static get AUTOCOMPLETE_FIELD() { return 'name'; } static get ASSOCIATIONS() { return []; } static getFieldMapping(data) { return { id: data.id || undefined, name: data.name || null, slug: data.slug || null, description: data.description || null, logo_url: data.logo_url || null, favicon_url: data.favicon_url || null, og_image_url: data.og_image_url || null, }; } static get DEFAULT_INCLUDES() { return []; } static get ALL_INCLUDES() { return [ { association: 'project_memberships_project' }, { association: 'assets_project' }, { association: 'presigned_url_requests_project' }, { association: 'tour_pages_project' }, { association: 'project_audio_tracks_project' }, { association: 'publish_events_project' }, { association: 'pwa_caches_project' }, { association: 'access_logs_project' }, ]; } static async findBy(where, options = {}) { const transaction = options.transaction; const runtimeProjectSlug = getRuntimeProjectSlug(options); const queryWhere = { ...where }; // Runtime access: filter by project slug // Skip if finding by ID (unambiguous lookup) if (runtimeProjectSlug && !where.id) { queryWhere.slug = runtimeProjectSlug; } const include = options.include !== undefined ? options.include : this.DEFAULT_INCLUDES; const record = await this.MODEL.findOne({ where: queryWhere, transaction, include, }); if (!record) return null; return record.get({ plain: true }); } /** * Create a new project and auto-snapshot global element defaults */ static async create(data, options = {}) { const transaction = options.transaction; // Create the project using parent's create const project = await super.create(data, options); // Auto-snapshot global element defaults to the new project // Errors propagate to service layer → transaction rollback → proper error to client const Project_element_defaultsDBApi = require('./project_element_defaults'); await Project_element_defaultsDBApi.snapshotGlobalDefaults(project.id, { ...options, transaction, }); return project; } static async findAll(filter = {}, options = {}) { filter = filter || {}; const limit = filter.limit || 0; const currentPage = +filter.page || 0; const offset = currentPage * limit; let where = {}; let include = []; 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 }; } } // Runtime access: filter by project slug const runtimeProjectSlug = getRuntimeProjectSlug(options); if (runtimeProjectSlug) { where.slug = runtimeProjectSlug; } 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 = ProjectsDBApi;