"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "Device", {
    enumerable: true,
    get: function() {
        return Device;
    }
});
const _lodash = require("lodash");
const _sequelize = require("sequelize");
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _utils = require("@tamanu/shared/utils");
const _Model = require("./Model");
const BASE_DEVICE_REGISTRATION_ADVISORY_KEY = 'deviceRegistrationAdvisoryLock';
let Device = class Device extends _Model.Model {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    static initModel({ primaryKey, ...options }) {
        super.init({
            id: {
                type: _sequelize.DataTypes.TEXT,
                allowNull: false,
                primaryKey: true,
                defaultValue: _sequelize.Sequelize.fn('gen_random_uuid')
            },
            lastSeenAt: {
                type: _sequelize.DataTypes.DATE,
                allowNull: false,
                defaultValue: _sequelize.Sequelize.fn('now')
            },
            scopes: {
                type: _sequelize.DataTypes.JSONB,
                allowNull: false,
                defaultValue: []
            },
            name: {
                type: _sequelize.DataTypes.TEXT
            }
        }, {
            ...options,
            syncDirection: _constants.SYNC_DIRECTIONS.DO_NOT_SYNC,
            validate: {
                mustHaveKnownScopes () {
                    if (// `as` necessary because of incomplete typings in sequelize
                    this.scopes.some((scope)=>!Object.values(_constants.DEVICE_SCOPES).includes(scope))) {
                        throw new _errors.InvalidOperationError('Device must only use known scopes.');
                    }
                }
            }
        });
    }
    static initRelations(models) {
        this.belongsTo(models.User, {
            foreignKey: 'registeredById',
            as: 'registeredBy'
        });
    }
    static async getQuotaByUserId(userId) {
        return this.count({
            where: {
                registeredById: userId,
                [_sequelize.Op.or]: _constants.DEVICE_SCOPES_SUBJECT_TO_QUOTA.map((scope)=>// the jsonb operator `?`: "Does the string exist as a top-level key within the JSON value?"
                    _sequelize.Sequelize.literal(`scopes ? '${scope}'`))
            }
        });
    }
    async markSeen() {
        await this.update({
            lastSeenAt: _sequelize.Sequelize.fn('now')
        });
    }
    requiresQuota() {
        return this.scopes.some((scope)=>_constants.DEVICE_SCOPES_SUBJECT_TO_QUOTA.includes(scope));
    }
    ensureHasScope(scope) {
        if (!this.scopes.includes(scope)) {
            throw new Error('Device must have the required scope.');
        }
    }
    static async ensureRegistration({ settings, user, deviceId, scopes }) {
        // There's two race conditions we seek to avoid with this transaction:
        //
        // 1. If the device logs in to the server twice simultaneously, we want
        //    to avoid two SyncDevice records being created. This is also mitigated
        //    by the UNIQUE constraint on the table.
        //
        // 2. If two unknown devices try to register at the same time, and the
        //    quota is reached on the first one, the second should not be able to.
        return await Device.sequelize.transaction({
            isolationLevel: _sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE
        }, async ()=>{
            if (!deviceId) {
                if (scopes.includes(_constants.DEVICE_SCOPES.SYNC_CLIENT)) {
                    throw new _errors.MissingCredentialError('Device ID is required');
                } else {
                    return;
                }
            }
            // If the same user tries to register two devices concurrently, the second will wait until the first is complete.
            await Device.acquireRegistrationLockForUser(user.id);
            const syncDevice = await Device.findByPk(deviceId);
            if (syncDevice) {
                if ((0, _lodash.difference)(scopes, syncDevice.scopes).length > 0) {
                    throw new _errors.AuthPermissionError('Requested more scopes than the device has');
                }
                await syncDevice.markSeen();
                return syncDevice;
            }
            const device = new Device({
                id: deviceId,
                registeredById: user.id,
                scopes
            });
            const deviceRegistrationQuotaEnabled = await settings.get(_constants.SETTING_KEYS.FEATURES_DEVICE_REGISTRATION_QUOTA_ENABLED);
            if (deviceRegistrationQuotaEnabled && device.requiresQuota()) {
                const currentCount = await Device.getQuotaByUserId(user.id);
                if (currentCount + 1 > user.deviceRegistrationQuota) {
                    throw new _errors.QuotaExceededError();
                }
            }
            await device.save();
            return device;
        });
    }
    static async acquireRegistrationLockForUser(userId) {
        const lockId = (0, _utils.stringToStableInteger)(`${BASE_DEVICE_REGISTRATION_ADVISORY_KEY}:${userId}`);
        await Device.sequelize.query(`SELECT pg_advisory_xact_lock(:lockId)`, {
            replacements: {
                lockId
            }
        });
    }
};

//# sourceMappingURL=Device.js.map