services/LineupService.jsjavascript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("../utils/errors.js");
const lineupConflictGuard_1 = require("../utils/lineupConflictGuard.js");
const transactionContext_1 = require("../utils/transactionContext.js");
const lineup_1 = __importDefault(require("../db/models/lineup"));
const ChoreoService_1 = __importDefault(require("./ChoreoService"));
const accessControl_1 = require("../utils/accessControl");
const stripProtectedFields_1 = require("../utils/stripProtectedFields.js");
const { logger } = require("../plugins/winston");
const { Op } = require("sequelize");
/**
 * Service for managing lineup entities and their associations.
 * Handles CRUD operations and lineup-specific logic.
 *
 * @class LineupService
 */
class LineupService {
    /**
     * Create a new lineup.
     * @param {number} startCount - The start count of the lineup.
     * @param {number} endCount - The end count of the lineup.
     * @param {UUID} ChoreoId - The choreography ID.
     * @param {UUID} actingUserId - The acting user ID.
     * @returns {Promise<Object>} The created lineup.
     */
    async create(startCount, endCount, ChoreoId, actingUserId, isAdmin = false) {
        logger.debug(`LineupService create ${JSON.stringify({
            startCount,
            endCount,
            ChoreoId,
            actingUserId,
            isAdmin,
        })}`);
        // Inherit ownerId from parent Choreo
        const choreo = await ChoreoService_1.default.findById(ChoreoId, actingUserId, isAdmin);
        if (!choreo) {
            throw new errors_1.NotFoundError(`Choreo with ID ${ChoreoId} not found`);
        }
        const ownerId = choreo.UserId;
        await (0, accessControl_1.checkWriteAccess)(ownerId, actingUserId, isAdmin);
        return (0, lineupConflictGuard_1.withLineupConflictGuard)([ChoreoId], async () => {
            const transaction = (0, transactionContext_1.getTransaction)();
            const lineup = await lineup_1.default.create({
                startCount,
                endCount,
                ChoreoId,
                UserId: ownerId,
                creatorId: actingUserId,
                updaterId: actingUserId,
            }, { transaction });
            await ChoreoService_1.default.update(ChoreoId, {}, actingUserId, isAdmin, { all: false }, transaction);
            return lineup;
        });
    }
    /**
     * Find or create a lineup.
     * @param {number} x - The x-coordinate of the lineup.
     * @param {number} y - The y-coordinate of the lineup.
     * @param {UUID} LineupId - The lineup ID associated with the lineup.
     * @param {UUID} MemberId - The member ID associated with the lineup.
     * @param {UUID} actingUserId - The acting user ID.
     * @param {Date} [timeOfManualUpdate=new Date()]
     * @returns {Promise<Object>} The found or created lineup.
     */
    async findOrCreate(startCount, endCount, ChoreoId, actingUserId, isAdmin = false) {
        logger.info(`LineupService findOrCreate ${JSON.stringify({
            startCount,
            endCount,
            ChoreoId,
            actingUserId,
            isAdmin,
        })}`);
        // Inherit ownerId from parent Choreo
        const choreo = await ChoreoService_1.default.findById(ChoreoId, actingUserId, isAdmin);
        if (!choreo) {
            logger.error(`Choreo with ID ${ChoreoId} not found when trying to findOrCreate lineup`);
            throw new errors_1.NotFoundError(`Choreo with ID ${ChoreoId} not found`);
        }
        const ownerId = choreo.UserId;
        await (0, accessControl_1.checkWriteAccess)(ownerId, actingUserId, isAdmin);
        logger.info(`Finding or creating lineup with startCount ${startCount}, endCount ${endCount}, ChoreoId ${ChoreoId}, ownerId ${ownerId}`);
        const [lineup, _created] = await (0, lineupConflictGuard_1.withLineupConflictGuard)([ChoreoId], async () => {
            const [found, created] = await lineup_1.default.findOrCreate({
                where: { startCount, endCount, ChoreoId, UserId: ownerId },
                defaults: {
                    startCount,
                    endCount,
                    ChoreoId,
                    UserId: ownerId,
                    creatorId: actingUserId,
                    updaterId: actingUserId,
                },
                transaction: (0, transactionContext_1.getTransaction)(),
            });
            return [found, created];
        });
        logger.info(`Lineup findOrCreate result: ${JSON.stringify({
            lineup: lineup.toJSON(),
            created: _created,
        })}`);
        return lineup;
    }
    /**
     * Update a lineup's count range.
     *
     * Rejects any attempt to move the lineup to another choreography. When the
     * count range actually changes, the update runs under the lineup conflict
     * guard so it is rejected if it would overlap another lineup or put a member
     * in two overlapping lineups; a timestamp-only update skips the guard.
     *
     * @param {UUID} id - The lineup ID.
     * @param {Object} data - Fields to update (e.g. `startCount`, `endCount`).
     * @param {UUID} actingUserId - The acting user ID.
     * @param {boolean} [isAdmin=false] - Whether the acting user is an admin.
     * @returns {Promise<Object>} The updated lineup with its positions and members.
     * @throws {NotFoundError} If no lineup exists with the given ID.
     * @throws {FaultyInputError} If `data` contains `ChoreoId`/`choreoId`
     * (lineups cannot be moved between choreographies).
     * @throws {LineupConflictError} If a range change introduces a lineup overlap
     * or member conflict.
     */
    async update(id, data, actingUserId, isAdmin = false) {
        logger.debug(`LineupService update ${JSON.stringify({ id, data, actingUserId, isAdmin })}`);
        const lineup = await lineup_1.default.findByPk(id);
        if (!lineup) {
            throw new errors_1.NotFoundError(`No lineup found with ID ${id} when updating`);
        }
        await (0, accessControl_1.checkWriteAccess)(lineup.UserId, actingUserId, isAdmin);
        const updates = (0, stripProtectedFields_1.stripProtectedUpdateFields)(data);
        if (Object.prototype.hasOwnProperty.call(updates, "ChoreoId") ||
            Object.prototype.hasOwnProperty.call(updates, "choreoId")) {
            throw new errors_1.FaultyInputError("Lineups cannot be moved to another choreography");
        }
        const applyUpdate = async () => {
            const transaction = (0, transactionContext_1.getTransaction)();
            await lineup.update({
                ...updates,
                updaterId: actingUserId,
            }, { transaction });
            await ChoreoService_1.default.update(lineup.ChoreoId, {}, actingUserId, isAdmin, { all: false }, transaction);
            return lineup_1.default.findOne({
                where: { id },
                include: [
                    {
                        association: "Positions",
                        include: [
                            {
                                association: "Member",
                            },
                        ],
                    },
                ],
                transaction,
            }); // njsscan-ignore: node_nosqli_injection
        };
        const rangeChanged = (Object.prototype.hasOwnProperty.call(updates, "startCount") &&
            updates.startCount !== lineup.startCount) ||
            (Object.prototype.hasOwnProperty.call(updates, "endCount") &&
                updates.endCount !== lineup.endCount);
        if (!rangeChanged) {
            return applyUpdate();
        }
        // Only a range change can create or resolve an overlap, so a timestamp-only
        // touch skips the guard and its two snapshot queries.
        return (0, lineupConflictGuard_1.withLineupConflictGuard)([lineup.ChoreoId], applyUpdate);
    }
    /**
     * Bump a lineup's timestamp without touching its range.
     * @param {UUID} id - The lineup ID.
     * @param {UUID} actingUserId - The acting user ID.
     * @returns {Promise<Object>} The updated lineup.
     */
    async touch(id, actingUserId, isAdmin = false) {
        return this.update(id, {}, actingUserId, isAdmin);
    }
    /**
     * Find a lineup by ID.
     * @param {UUID} id - The lineup ID.
     * @param {UUID[]} ownerIds - Array of owner IDs the acting user has access to.
     * @param {UUID} actingUserId - The acting user ID.
     * @returns {Promise<Object>} The found lineup.
     */
    async findById(id, actingUserId, isAdmin = false) {
        logger.debug(`LineupService findById ${JSON.stringify({ id, actingUserId, isAdmin })}`);
        const lineup = await lineup_1.default.findByPk(id);
        if (!lineup) {
            return null;
        }
        await (0, accessControl_1.checkReadAccess)(lineup.UserId, actingUserId, isAdmin);
        return lineup_1.default.findOne({ where: { id } }); // njsscan-ignore: node_nosqli_injection
    }
    /**
     * Get all lineups for a choreography.
     * @param {UUID} ChoreoId - The choreography ID.
     * @returns {Promise<Array>} List of lineups.
     */
    async findByChoreoId(ChoreoId) {
        logger.debug(`LineupService findByChoreoId ${JSON.stringify({ ChoreoId })}`);
        return lineup_1.default.findAll({ where: { ChoreoId } });
    }
    /**
     * Remove a lineup.
     * @param {UUID} id - The lineup ID.
     * @param {UUID} actingUserId - The acting user ID.
     * @returns {Promise<void>} Resolves when the lineup is removed.
     */
    async remove(id, actingUserId, isAdmin = false) {
        logger.debug(`LineupService remove ${JSON.stringify({ id, actingUserId, isAdmin })}`);
        const lineup = await lineup_1.default.findByPk(id);
        if (!lineup) {
            throw new errors_1.NotFoundError(`No lineup found with ID ${id} when deleting`);
        }
        await (0, accessControl_1.checkDeleteAccess)(lineup.UserId, actingUserId, isAdmin);
        return (0, lineupConflictGuard_1.withLineupConflictGuard)([lineup.ChoreoId], async () => {
            const transaction = (0, transactionContext_1.getTransaction)();
            await ChoreoService_1.default.update(lineup.ChoreoId, {}, actingUserId, isAdmin, { all: false }, transaction);
            return lineup.destroy({ transaction });
        });
    }
    async migrateCreatorUpdater() {
        logger.debug(`LineupService migrateCreatorUpdater`);
        const lineups = await lineup_1.default.findAll({
            where: { creatorId: { [Op.is]: null }, UserId: { [Op.not]: null } },
        });
        await Promise.all(lineups.map((lineup) => lineup.update({ creatorId: lineup.UserId, updaterId: lineup.UserId })));
    }
}
exports.default = new LineupService();