"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var PluginRepository_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginRepository = void 0;
const extism_1 = require("@extism/extism");
const common_1 = require("@nestjs/common");
const generic_pool_1 = require("generic-pool");
const kysely_1 = require("kysely");
const postgres_1 = require("kysely/helpers/postgres");
const nestjs_kysely_1 = require("nestjs-kysely");
const database_1 = require("../database");
const decorators_1 = require("../decorators");
const plugin_dto_1 = require("../dtos/plugin.dto");
const enum_1 = require("../enum");
const logging_repository_1 = require("./logging.repository");
const levels = {
    [enum_1.LogLevel.Verbose]: 'trace',
    [enum_1.LogLevel.Debug]: 'debug',
    [enum_1.LogLevel.Log]: 'info',
    [enum_1.LogLevel.Warn]: 'warn',
    [enum_1.LogLevel.Error]: 'error',
    [enum_1.LogLevel.Fatal]: 'error',
};
const asExtismLogLevel = (logLevel) => levels[logLevel] || 'info';
let PluginRepository = PluginRepository_1 = class PluginRepository {
    db;
    logger;
    pluginMap = new Map();
    constructor(db, logger) {
        this.db = db;
        this.logger = logger;
        this.logger.setContext(PluginRepository_1.name);
    }
    getForLoad() {
        return this.db
            .selectFrom('plugin')
            .select((eb) => [
            'plugin.id',
            'plugin.name',
            'plugin.version',
            'plugin.wasmBytes',
            (0, postgres_1.jsonArrayFrom)(eb
                .selectFrom('plugin_method')
                .whereRef('plugin_method.pluginId', '=', 'plugin.id')
                .select(['plugin_method.name', 'plugin_method.hostFunctions'])).as('methods'),
        ])
            .where('enabled', '=', true)
            .execute();
    }
    queryBuilder() {
        return this.db.selectFrom('plugin').select((eb) => [
            'plugin.id',
            'plugin.name',
            'plugin.title',
            'plugin.description',
            'plugin.author',
            'plugin.version',
            'plugin.createdAt',
            'plugin.updatedAt',
            'plugin.templates',
            (0, postgres_1.jsonArrayFrom)(eb
                .selectFrom('plugin_method')
                .select([...database_1.columns.pluginMethod, 'plugin.name as pluginName'])
                .whereRef('plugin_method.pluginId', '=', 'plugin.id')).as('methods'),
        ]);
    }
    search(dto = {}) {
        return this.queryBuilder()
            .$if(!!dto.id, (qb) => qb.where('plugin.id', '=', dto.id))
            .$if(!!dto.name, (qb) => qb.where('plugin.name', '=', dto.name))
            .$if(!!dto.title, (qb) => qb.where('plugin.title', '=', dto.title))
            .$if(!!dto.description, (qb) => qb.where('plugin.description', '=', dto.description))
            .$if(!!dto.version, (qb) => qb.where('plugin.version', '=', dto.version))
            .orderBy('plugin.name')
            .execute();
    }
    getByHash(hash) {
        return this.queryBuilder().where('plugin.sha256hash', '=', hash).executeTakeFirst();
    }
    getByName(name) {
        return this.queryBuilder().where('plugin.name', '=', name).executeTakeFirst();
    }
    get(id) {
        return this.queryBuilder().where('plugin.id', '=', id).executeTakeFirst();
    }
    getForValidation() {
        return this.db
            .selectFrom('plugin_method')
            .innerJoin('plugin', 'plugin_method.pluginId', 'plugin.id')
            .select(['plugin_method.id', 'plugin_method.name', 'plugin.name as pluginName', 'plugin_method.types'])
            .execute();
    }
    searchMethods(dto = {}) {
        return this.db
            .selectFrom('plugin_method')
            .innerJoin('plugin', 'plugin.id', 'plugin_method.pluginId')
            .select(['plugin.name as pluginName', 'plugin_method.pluginId', 'plugin_method.id', ...database_1.columns.pluginMethod])
            .$if(!!dto.id, (qb) => qb.where('plugin_method.id', '=', dto.id))
            .$if(!!dto.name, (qb) => qb.where('plugin_method.name', '=', dto.name))
            .$if(!!dto.title, (qb) => qb.where('plugin_method.title', '=', dto.title))
            .$if(!!dto.type, (qb) => qb.where('plugin_method.types', '@>', [dto.type]))
            .$if(!!dto.description, (qb) => qb.where('plugin_method.description', '=', dto.description))
            .$if(!!dto.pluginVersion, (qb) => qb.where('plugin.version', '=', dto.pluginVersion))
            .$if(!!dto.pluginName, (qb) => qb.where('plugin.name', '=', dto.pluginName))
            .orderBy('plugin_method.name')
            .execute();
    }
    async upsert(dto, initialMethods) {
        return this.db.transaction().execute(async (tx) => {
            const plugin = await tx
                .insertInto('plugin')
                .values(dto)
                .onConflict((oc) => oc.columns(['name', 'version']).doUpdateSet((eb) => ({
                title: eb.ref('excluded.title'),
                description: eb.ref('excluded.description'),
                author: eb.ref('excluded.author'),
                version: eb.ref('excluded.version'),
                wasmBytes: eb.ref('excluded.wasmBytes'),
                templates: eb.ref('excluded.templates'),
                sha256hash: eb.ref('excluded.sha256hash'),
            })))
                .returning(['id', 'name'])
                .executeTakeFirstOrThrow();
            if (initialMethods.length > 0) {
                await tx
                    .deleteFrom('plugin_method')
                    .where('plugin_method.pluginId', '=', plugin.id)
                    .where('name', 'not in', initialMethods.map((method) => method.name))
                    .execute();
            }
            const methods = initialMethods.length > 0
                ? await tx
                    .insertInto('plugin_method')
                    .values(initialMethods.map((method) => ({ ...method, pluginId: plugin.id })))
                    .onConflict((oc) => oc.columns(['pluginId', 'name']).doUpdateSet(({ ref }) => ({
                    pluginId: ref('excluded.pluginId'),
                    name: ref('excluded.name'),
                    title: ref('excluded.title'),
                    description: ref('excluded.description'),
                    types: ref('excluded.types'),
                    hostFunctions: ref('excluded.hostFunctions'),
                    allowedHosts: ref('excluded.allowedHosts'),
                    uiHints: ref('excluded.uiHints'),
                    schema: ref('excluded.schema'),
                })))
                    .returningAll()
                    .execute()
                : [];
            return { ...plugin, methods };
        });
    }
    async load({ key, label, wasmBytes }, { runInWorker, functions }) {
        const data = new Uint8Array(wasmBytes.buffer, wasmBytes.byteOffset, wasmBytes.byteLength);
        const logger = logging_repository_1.LoggingRepository.create(`Plugin:${label}`);
        const pool = (0, generic_pool_1.createPool)({
            create: () => (0, extism_1.newPlugin)({ wasm: [{ data }] }, {
                useWasi: true,
                runInWorker,
                functions: {
                    'extism:host/user': functions ?? {},
                },
                logger: {
                    trace: (message) => logger.verbose(message),
                    info: (message) => logger.log(message),
                    debug: (message) => logger.debug(message),
                    warn: (message) => logger.warn(message),
                    error: (message) => logger.error(message),
                },
                logLevel: asExtismLogLevel(logger.getLogLevel()),
                enableWasiOutput: true,
            }),
            destroy: (plugin) => plugin.close(),
        }, { min: 1, max: 5 });
        try {
            await pool.ready();
            this.pluginMap.set(key, { pool, label });
        }
        catch (error) {
            throw new Error(`Unable to instantiate plugin: ${key}`, { cause: error });
        }
    }
    async callMethod({ pluginKey, methodName }, input, context) {
        const item = this.pluginMap.get(pluginKey);
        if (!item) {
            throw new Error(`No loaded plugin found for ${pluginKey}`);
        }
        const { pool, label } = item;
        try {
            const plugin = await pool.acquire();
            try {
                const result = await plugin.call(methodName, JSON.stringify(input), context);
                return (result ? result.json() : result);
            }
            finally {
                await pool.release(plugin);
            }
        }
        catch (error) {
            throw new Error(`Plugin method call failed: ${label}#${methodName}`, { cause: error });
        }
    }
};
exports.PluginRepository = PluginRepository;
__decorate([
    (0, decorators_1.GenerateSql)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "getForLoad", null);
__decorate([
    (0, decorators_1.GenerateSql)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [plugin_dto_1.PluginSearchDto]),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "search", null);
__decorate([
    (0, decorators_1.GenerateSql)({ params: [decorators_1.DummyValue.STRING] }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Buffer]),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "getByHash", null);
__decorate([
    (0, decorators_1.GenerateSql)({ params: [decorators_1.DummyValue.STRING] }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "getByName", null);
__decorate([
    (0, decorators_1.GenerateSql)({ params: [decorators_1.DummyValue.UUID] }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "get", null);
__decorate([
    (0, decorators_1.GenerateSql)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], PluginRepository.prototype, "getForValidation", null);
__decorate([
    (0, decorators_1.GenerateSql)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [plugin_dto_1.PluginMethodSearchDto]),
    __metadata("design:returntype", void 0)
], PluginRepository.prototype, "searchMethods", null);
exports.PluginRepository = PluginRepository = PluginRepository_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, nestjs_kysely_1.InjectKysely)()),
    __metadata("design:paramtypes", [kysely_1.Kysely,
        logging_repository_1.LoggingRepository])
], PluginRepository);
//# sourceMappingURL=plugin.repository.js.map