"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: Object.getOwnPropertyDescriptor(all, name).get
    });
}
_export(exports, {
    get findRouteObject () {
        return findRouteObject;
    },
    get getResourceList () {
        return getResourceList;
    },
    get paginatedGetList () {
        return paginatedGetList;
    },
    get permissionCheckingRouter () {
        return permissionCheckingRouter;
    },
    get runPaginatedQuery () {
        return runPaginatedQuery;
    },
    get simpleGet () {
        return simpleGet;
    },
    get simpleGetHasOne () {
        return simpleGetHasOne;
    },
    get simpleGetList () {
        return simpleGetList;
    },
    get simplePatch () {
        return simplePatch;
    },
    get simplePost () {
        return simplePost;
    },
    get simplePut () {
        return simplePut;
    },
    get softDeletionCheckingRouter () {
        return softDeletionCheckingRouter;
    }
});
const _express = /*#__PURE__*/ _interop_require_default(require("express"));
const _expressasynchandler = /*#__PURE__*/ _interop_require_default(require("express-async-handler"));
const _lodash = require("lodash");
const _sequelize = require("sequelize");
const _zod = require("zod");
const _errors = require("@tamanu/errors");
const _renameObjectKeys = require("@tamanu/utils/renameObjectKeys");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
const permissionCheckingRouter = (action, subject)=>{
    const router = _express.default.Router();
    router.use((req, res, next)=>{
        req.checkPermission(action, subject);
        next();
    });
    return router;
};
const softDeletionCheckingRouter = (tableName)=>{
    const router = _express.default.Router();
    router.use(async (req, res, next)=>{
        const { models, body, params } = req;
        const id = params.id || body.id;
        if (!id) {
            next();
            return;
        }
        const object = await models[tableName].findByPk(id, {
            paranoid: false
        });
        if (object && object.deletedAt) {
            throw new _errors.InvalidOperationError(`Invalid Operation Error: Cannot update a deleted ${tableName}, id: ${id}.`);
        }
    });
    return router;
};
const findRouteObject = async (req, modelName, options = {})=>{
    const { models, params } = req;
    const { additionalFilters = {} } = options;
    const model = models[modelName];
    // check the user can read this model type before searching for it
    // (otherwise, they can see if they get a "not permitted" or a
    // "not found" to snoop for objects)
    req.checkPermission('read', modelName);
    const object = await model.findByPk(params.id, {
        include: model.getFullReferenceAssociations(),
        where: {
            ...additionalFilters
        }
    });
    if (!object) throw new _errors.NotFoundError();
    req.checkPermission('read', object);
    return object;
};
const simpleGet = (modelName, options = {})=>(0, _expressasynchandler.default)(async (req, res)=>{
        const { auditAccess = false } = options;
        const { models, params, query } = req;
        const object = await findRouteObject(req, modelName);
        if (auditAccess && object) {
            await req.audit.access({
                recordId: object.id,
                frontEndContext: params,
                model: models[modelName],
                facilityId: query.facilityId
            });
        }
        res.send(object);
    });
const simpleGetHasOne = (modelName, foreignKey, options = {}, transform = undefined)=>(0, _expressasynchandler.default)(async (req, res)=>{
        const { models, params } = req;
        const model = models[modelName];
        const { additionalFilters = {}, auditAccess = false } = options;
        req.checkPermission('read', modelName);
        const object = await model.findOne({
            where: {
                [foreignKey]: params.id,
                ...additionalFilters
            },
            include: model.getFullReferenceAssociations()
        });
        if (!object) throw new _errors.NotFoundError();
        if (auditAccess && object) {
            await req.audit.access({
                recordId: object.id,
                frontEndContext: params,
                model: models[modelName]
            });
        }
        res.send(transform ? transform(object) : object);
    });
const conjoiner = new Intl.ListFormat();
function validatePatchAllowedFields(model, allowedFields) {
    const valids = new Set(Object.keys(model?.rawAttributes ?? {}));
    for (const field of [
        'createdAt',
        'deletedAt',
        'updatedAt',
        'updatedAtSyncTick'
    ]){
        valids.delete(field);
    }
    const invalids = allowedFields.filter((field)=>!valids.has(field));
    if (invalids.length > 0) {
        const unit = invalids.length === 1 ? 'field' : 'fields';
        throw new _errors.UsageError(`simplePatch allowedFields option includes invalid ${unit} for ${model.name}: ${conjoiner.format(invalids)}. (Permitted fields: ${conjoiner.format(valids)}.)`);
    }
}
async function validatePatchBody(allowedFields, req) {
    const parsed = await _zod.z.object(Object.fromEntries(allowedFields.map((key)=>[
            key,
            _zod.z.unknown()
        ]))).partial().strict().safeParseAsync(req.body);
    if (parsed.success) return;
    const disallowed = parsed.error.issues.filter((issue)=>issue.code === 'unrecognized_keys').flatMap((issue)=>issue.keys);
    if (disallowed.length > 0) {
        const unit = disallowed.length === 1 ? 'field' : 'fields';
        throw new _errors.InvalidOperationError(`PATCH body includes disallowed ${unit}: ${conjoiner.format(disallowed)}. (Allowed fields: ${conjoiner.format(allowedFields)}.)`);
    }
    const [first] = parsed.error.issues;
    throw new _errors.InvalidOperationError(first?.message ?? 'Invalid PATCH body');
}
const simplePatch = (modelName, options)=>{
    if (!options?.allowedFields || options.allowedFields.length === 0) {
        throw new _errors.InvalidOperationError('simplePatch requires a nonempty allowedFields option');
    }
    return (0, _expressasynchandler.default)(async (req, res)=>{
        req.checkPermission('read', modelName);
        const { allowedFields } = options;
        const { models: { [modelName]: model }, params: { id } } = req;
        if (process.env.NODE_ENV !== 'production') validatePatchAllowedFields(model, allowedFields);
        if (req.body == null) throw new _errors.InvalidOperationError('PATCH body is required');
        // Optimistically assume body is valid and begin fetching object before validated
        const [, object] = await Promise.all([
            validatePatchBody(allowedFields, req),
            model.findByPk(id)
        ]);
        if (!object) throw new _errors.NotFoundError(`No ${modelName} found with ID ${id}`);
        if (object.deletedAt) {
            throw new _errors.InvalidOperationError(`Cannot update deleted ${modelName} with ID ${id}`);
        }
        if (Object.hasOwn(req.body, 'deletedAt')) {
            throw new _errors.InvalidOperationError('Cannot update deletedAt field with PATCH request');
        }
        req.checkPermission('write', object);
        await object.update((0, _lodash.pick)(req.body, allowedFields));
        res.send(object);
    });
};
const simplePut = (modelName)=>(0, _expressasynchandler.default)(async (req, res)=>{
        const { models, params } = req;
        req.checkPermission('read', modelName);
        const object = await models[modelName].findByPk(params.id);
        if (!object) throw new _errors.NotFoundError();
        if (object.deletedAt) throw new _errors.InvalidOperationError(`Cannot update deleted object with id (${params.id}), you need to restore it first`);
        if (Object.prototype.hasOwnProperty.call(req.body, 'deletedAt')) throw new _errors.InvalidOperationError('Cannot update deletedAt field');
        req.checkPermission('write', object);
        await object.update(req.body);
        res.send(object);
    });
const simplePost = (modelName, options = {})=>(0, _expressasynchandler.default)(async (req, res)=>{
        const { models } = req;
        const { skipPermissionCheck = false } = options;
        if (skipPermissionCheck === false) {
            req.checkPermission('create', modelName);
        }
        const existingObject = await models[modelName].findByPk(req.body.id, {
            paranoid: false
        });
        if (existingObject) {
            throw new _errors.InvalidOperationError(`Cannot create object with id (${req.body.id}), it already exists`);
        }
        const object = await models[modelName].create(req.body);
        res.send(object);
    });
const getResourceList = async (req, modelName, foreignKey = '', options = {})=>{
    const { models, params, query } = req;
    const { order = 'ASC', orderBy = 'createdAt', rowsPerPage, page } = query;
    const { additionalFilters = {}, include = [], skipPermissionCheck = false } = options;
    if (skipPermissionCheck === false) {
        req.checkPermission('list', modelName);
    }
    const model = models[modelName];
    const associations = model.getListReferenceAssociations(models) || [];
    const baseQueryOptions = {
        where: {
            ...foreignKey && {
                [foreignKey]: params.id
            },
            ...additionalFilters
        },
        // ['association', 'column', 'direction'] is the sequlize format to sort by foreign column
        // allow 'association.column' as a valid sort query
        order: orderBy ? [
            [
                ...orderBy.split('.'),
                order.toUpperCase()
            ]
        ] : undefined,
        include: [
            ...associations,
            ...include
        ]
    };
    const count = await models[modelName].count({
        ...baseQueryOptions,
        distinct: true
    });
    const objects = await models[modelName].findAll({
        ...baseQueryOptions,
        limit: rowsPerPage,
        offset: page && rowsPerPage ? page * rowsPerPage : undefined
    });
    const data = objects.map((x)=>x.forResponse());
    return {
        count,
        data
    };
};
const simpleGetList = (modelName, foreignKey = '', options = {})=>(0, _expressasynchandler.default)(async (req, res)=>{
        const response = await getResourceList(req, modelName, foreignKey, options);
        res.send(response);
    });
const paginatedGetList = (modelName, foreignKey = '', options = {})=>{
    const { additionalFilters = {}, include = [], skipPermissionCheck = false } = options;
    return (0, _expressasynchandler.default)(async (req, res)=>{
        const { models, params, query } = req;
        const { page = 0, order = 'ASC', orderBy, rowsPerPage } = query;
        const offset = query.offset || page * rowsPerPage || 0;
        if (skipPermissionCheck === false) {
            req.checkPermission('list', modelName);
        }
        const model = models[modelName];
        const associations = model.getListReferenceAssociations(models) || [];
        const queryOpts = {
            where: {
                ...foreignKey && {
                    [foreignKey]: params.id
                },
                ...additionalFilters
            },
            include: [
                ...associations,
                ...include
            ]
        };
        const resultsToCount = await models[modelName].findAll(queryOpts);
        const count = resultsToCount.length;
        // Exit early if there are no results
        if (count === 0) {
            res.send({
                count,
                data: []
            });
            return;
        }
        const objects = await models[modelName].findAll({
            ...queryOpts,
            // ['association', 'column', 'direction'] is the sequlize format to sort by foreign column
            // allow 'association.column' as a valid sort query
            order: orderBy ? [
                [
                    ...orderBy.split('.'),
                    order.toUpperCase()
                ]
            ] : undefined,
            limit: rowsPerPage || undefined,
            offset
        });
        const data = objects.map((x)=>x.forResponse());
        res.send({
            count: resultsToCount.length,
            data
        });
    });
};
async function runPaginatedQuery(db, model, countQuery, selectQuery, params, pagination) {
    const countResult = await db.query(countQuery, {
        replacements: params,
        type: _sequelize.QueryTypes.SELECT
    });
    const { count } = countResult[0];
    if (count === 0) {
        return {
            data: [],
            count: 0
        };
    }
    const { page = 0, rowsPerPage = 10 } = pagination;
    const result = await db.query(`${selectQuery} LIMIT :limit OFFSET :offset`, {
        replacements: {
            ...params,
            limit: rowsPerPage,
            offset: page * rowsPerPage
        },
        model,
        type: _sequelize.QueryTypes.SELECT,
        mapToModel: true
    });
    const forResponse = result.map((x)=>(0, _renameObjectKeys.renameObjectKeys)(x.forResponse()));
    return {
        count,
        data: forResponse
    };
}

//# sourceMappingURL=crudHelpers.js.map