import { Op, type Includeable, type InferAttributes, type InferCreationAttributes, type WhereAttributeHash, } from 'sequelize'; import db from '@/db/models'; import { removeRecord, deleteRecordsByIds, autocompleteByField, } from '@/db/api/shared/repository'; import { BULK_IMPORT_TIMESTAMP_STEP_MS } from '@/shared/constants/database'; import { resolvePagination } from '@/shared/constants/pagination'; import Utils from '@/db/utils'; import FileDBApi from '@/db/api/file'; import type { Messages } from '@/db/models/messages'; import type { CurrentUser, DbApiOptions, FileInput } from '@/db/api/types'; type MessagesData = Partial> & { organization?: string | null; campus?: string | null; sent_by?: string | null; attachments?: FileInput | FileInput[] | null; }; interface MessagesFilter { limit?: number | string; page?: number | string; id?: string; subject?: string; body?: string; sent_atRange?: Array; active?: boolean | string; channel?: string; audience?: string; status?: string; campus?: string; sent_by?: string; organization?: string; createdAtRange?: Array; field?: string; sort?: string; } const NO_USER: CurrentUser = { id: null }; function messagesTableName(): string { const name = db.messages.getTableName(); return typeof name === 'string' ? name : name.tableName; } class MessagesDBApi { static async create( data: MessagesData, options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const messages = await db.messages.create( { id: data.id || undefined, subject: data.subject || null, body: data.body || null, channel: data.channel || null, audience: data.audience || null, sent_at: data.sent_at || null, status: data.status || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, }, { transaction }, ); await messages.setOrganization(currentUser.organizationId ?? undefined, { transaction, }); await messages.setCampus(data.campus ?? undefined, { transaction }); await messages.setSent_by(data.sent_by ?? undefined, { transaction }); await FileDBApi.replaceRelationFiles( { belongsTo: messagesTableName(), belongsToColumn: 'attachments', belongsToId: messages.id, }, data.attachments, options, ); return messages; } static async bulkImport( data: MessagesData[], options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const messagesData = data.map((item, index) => ({ id: item.id || undefined, subject: item.subject || null, body: item.body || null, channel: item.channel || null, audience: item.audience || null, sent_at: item.sent_at || null, status: item.status || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, createdAt: new Date(Date.now() + index * BULK_IMPORT_TIMESTAMP_STEP_MS), })); const messages = await db.messages.bulkCreate(messagesData, { transaction }); for (let i = 0; i < messages.length; i++) { await FileDBApi.replaceRelationFiles( { belongsTo: messagesTableName(), belongsToColumn: 'attachments', belongsToId: messages[i].id, }, data[i].attachments, options, ); } return messages; } static async update( id: string, data: MessagesData, options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const globalAccess = currentUser.app_role?.globalAccess; const messages = await db.messages.findByPk(id, { transaction }); if (!messages) { return null; } const updatePayload: Partial> = {}; if (data.subject !== undefined) updatePayload.subject = data.subject; if (data.body !== undefined) updatePayload.body = data.body; if (data.channel !== undefined) updatePayload.channel = data.channel; if (data.audience !== undefined) updatePayload.audience = data.audience; if (data.sent_at !== undefined) updatePayload.sent_at = data.sent_at; if (data.status !== undefined) updatePayload.status = data.status; updatePayload.updatedById = currentUser.id; await messages.update(updatePayload, { transaction }); if (data.organization !== undefined) { const orgId = globalAccess ? data.organization : currentUser.organizationId; await messages.setOrganization(orgId ?? undefined, { transaction }); } if (data.campus !== undefined) { await messages.setCampus(data.campus ?? undefined, { transaction }); } if (data.sent_by !== undefined) { await messages.setSent_by(data.sent_by ?? undefined, { transaction }); } await FileDBApi.replaceRelationFiles( { belongsTo: messagesTableName(), belongsToColumn: 'attachments', belongsToId: messages.id, }, data.attachments, options, ); return messages; } static async deleteByIds( ids: string[], options?: DbApiOptions, ): Promise { return deleteRecordsByIds(db.messages, ids, options); } static async remove( id: string, options?: DbApiOptions, ): Promise { return removeRecord(db.messages, id, options); } static async findBy( where: WhereAttributeHash, options?: DbApiOptions, ): Promise | null> { const transaction = options?.transaction; const messages = await db.messages.findOne({ where, transaction }); if (!messages) { return null; } const output: Record = messages.get({ plain: true }); const [ message_recipients_message, organization, campus, sent_by, attachments, ] = await Promise.all([ messages.getMessage_recipients_message({ transaction }), messages.getOrganization({ transaction }), messages.getCampus({ transaction }), messages.getSent_by({ transaction }), messages.getAttachments({ transaction }), ]); output.message_recipients_message = message_recipients_message; output.organization = organization; output.campus = campus; output.sent_by = sent_by; output.attachments = attachments; return output; } static async findAll( filter: MessagesFilter, globalAccess: boolean, options?: DbApiOptions, ): Promise<{ rows: Messages[]; count: number }> { const { limit, offset } = resolvePagination(filter.limit, filter.page); let where: WhereAttributeHash = {}; const userOrganizations = options?.currentUser?.organizations?.id ?? null; if (userOrganizations && options?.currentUser?.organizationId) { where.organizationId = options.currentUser.organizationId; } const include: Includeable[] = [ { model: db.organizations, as: 'organization' }, { model: db.campuses, as: 'campus', where: filter.campus ? { [Op.or]: [ { id: { [Op.in]: filter.campus.split('|').map((t) => Utils.uuid(t)), }, }, { name: { [Op.or]: filter.campus .split('|') .map((t) => ({ [Op.iLike]: `%${t}%` })), }, }, ], } : {}, }, { model: db.users, as: 'sent_by', where: filter.sent_by ? { [Op.or]: [ { id: { [Op.in]: filter.sent_by.split('|').map((t) => Utils.uuid(t)), }, }, { firstName: { [Op.or]: filter.sent_by .split('|') .map((t) => ({ [Op.iLike]: `%${t}%` })), }, }, ], } : {}, }, { model: db.file, as: 'attachments' }, ]; if (filter.id) { where = { ...where, id: Utils.uuid(filter.id) }; } if (filter.subject) { where = { ...where, [Op.and]: Utils.ilike('messages', 'subject', filter.subject), }; } if (filter.body) { where = { ...where, [Op.and]: Utils.ilike('messages', 'body', filter.body), }; } if (filter.sent_atRange) { const [start, end] = filter.sent_atRange; if (start !== undefined && start !== null && start !== '') { where = { ...where, sent_at: { [Op.gte]: start } }; } if (end !== undefined && end !== null && end !== '') { where = { ...where, sent_at: { ...(typeof where.sent_at === 'object' ? where.sent_at : {}), [Op.lte]: end, }, }; } } if (filter.active !== undefined) { where = { ...where, active: filter.active === true || filter.active === 'true', }; } if (filter.channel) { where = { ...where, channel: filter.channel }; } if (filter.audience) { where = { ...where, audience: filter.audience }; } if (filter.status) { where = { ...where, status: filter.status }; } if (filter.organization) { const listItems = filter.organization .split('|') .map((item) => Utils.uuid(item)); where = { ...where, organizationId: { [Op.or]: listItems } }; } if (filter.createdAtRange) { const [start, end] = filter.createdAtRange; if (start !== undefined && start !== null && start !== '') { where = { ...where, createdAt: { [Op.gte]: start } }; } if (end !== undefined && end !== null && end !== '') { where = { ...where, createdAt: { ...(typeof where.createdAt === 'object' ? where.createdAt : {}), [Op.lte]: end, }, }; } } if (globalAccess) { delete where.organizationId; } const order: [string, string][] = filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']]; const { rows, count } = await db.messages.findAndCountAll({ where, include, distinct: true, order, transaction: options?.transaction, limit: !options?.countOnly && limit ? limit : undefined, offset: !options?.countOnly && offset ? offset : undefined, }); return { rows: options?.countOnly ? [] : rows, count }; } static async findAllAutocomplete( query: string | undefined, limit: number | undefined, offset: number | undefined, globalAccess: boolean, organizationId: string | undefined, ): Promise> { return autocompleteByField( db.messages, 'subject', query, limit, offset, globalAccess, organizationId, ); } } export default MessagesDBApi;