"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.BaseEntity = exports.ItemNotFound = void 0;
exports.bindEntityDatabase = bindEntityDatabase;
exports.getEntityDatabase = getEntityDatabase;
exports.createEntity = createEntity;
exports.createEntities = createEntities;
exports.createQueuedEntity = createQueuedEntity;
exports.createQueuedEntities = createQueuedEntities;
exports.createPersistedEntity = createPersistedEntity;
exports.persistEntity = persistEntity;
exports.persistEntities = persistEntities;
exports.createPersistedEntities = createPersistedEntities;
exports.markEntityClean = markEntityClean;
exports.getEntityOriginal = getEntityOriginal;
exports.hasEntitySnapshot = hasEntitySnapshot;
exports.getDirtyDetails = getDirtyDetails;
exports.getDirtyFields = getDirtyFields;
exports.isEntityDirty = isEntityDirty;
exports.isFieldDirty = isFieldDirty;
exports.getFieldOriginal = getFieldOriginal;
exports.revertDirtyEntity = revertDirtyEntity;
exports.getEntityFields = getEntityFields;
exports.getPKFieldForEntity = getPKFieldForEntity;
exports.getPKFieldForEntityInstance = getPKFieldForEntityInstance;
exports.getKeyedEntities = getKeyedEntities;
exports.getKeyedGroupedEntities = getKeyedGroupedEntities;
exports.getEntitiesById = getEntitiesById;
exports.getEntitiesByIdWithKeyName = getEntitiesByIdWithKeyName;
exports.resolveRelated = resolveRelated;
exports.resolveRelatedByPivot = resolveRelatedByPivot;
exports.resolveRelatedByPivotForOne = resolveRelatedByPivotForOne;
exports.getEntityOrUndefined = getEntityOrUndefined;
exports.getEntity = getEntity;
exports.getEntityOr404 = getEntityOr404;
exports.entityExists = entityExists;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_util_1 = require("node:util");
const errors_1 = require("../http/errors");
const metadata_1 = require("./metadata");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":2},{\"kind\":22,\"type\":{\"kind\":3}}]]", []);
const EntityDatabase = new WeakMap();
const OriginalSnapshot = new WeakMap();
let _ItemNotFoundMetadata_1;
class ItemNotFound extends Error {
    constructor(message = 'Item not found') {
        super(message);
        this.name = 'ItemNotFound';
    }
    static __tsfType = _ItemNotFoundMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"ItemNotFound\",\"typeName\":\"ItemNotFound\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"message\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":true}]}]", [() => ItemNotFound], __tsf_metadata_type);
}
exports.ItemNotFound = ItemNotFound;
_ItemNotFoundMetadata_1.classType = ItemNotFound;
ItemNotFound.__tsfType = _ItemNotFoundMetadata_1;
let _BaseEntityMetadata_1;
class BaseEntity {
    static query(session) {
        return getEntityDatabase(this).query(this, session);
    }
    static getDatabase() {
        return getEntityDatabase(this);
    }
    static registerDatabase(db) {
        bindEntityDatabase(this, db);
    }
    static reference(value) {
        const data = typeof value === 'object' ? value : { [getPKFieldForEntity(this)]: value };
        const entity = createEntity(this, data);
        markEntityClean(entity);
        return entity;
    }
    async save(session) {
        await getEntityDatabase(this.constructor).saveEntity(this, session);
    }
    async delete(session) {
        await getEntityDatabase(this.constructor).deleteEntity(this, session);
    }
    async remove(session) {
        await this.delete(session);
    }
    static __tsfType = _BaseEntityMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"BaseEntity\",\"typeName\":\"BaseEntity\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"save\",\"parameters\":[{\"name\":\"session\",\"type\":{\"kind\":16,\"typeName\":\"DatabaseSession\",\"classType\":{\"$tsfImport\":[1,\"./session\",\"DatabaseSession\"]}},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":1}},{\"name\":\"delete\",\"parameters\":[{\"name\":\"session\",\"type\":{\"kind\":16,\"typeName\":\"DatabaseSession\",\"classType\":{\"$tsfImport\":[1,\"./session\",\"DatabaseSession\"]}},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":1}},{\"name\":\"remove\",\"parameters\":[{\"name\":\"session\",\"type\":{\"kind\":16,\"typeName\":\"DatabaseSession\",\"classType\":{\"$tsfImport\":[1,\"./session\",\"DatabaseSession\"]}},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":1}}],\"hasConstructor\":false,\"constructorParameters\":[]}]", [() => BaseEntity, require], __tsf_metadata_type);
}
exports.BaseEntity = BaseEntity;
_BaseEntityMetadata_1.classType = BaseEntity;
BaseEntity.__tsfType = _BaseEntityMetadata_1;
function bindEntityDatabase(entityClass, db) {
    EntityDatabase.set(entityClass, db);
}
function getEntityDatabase(entityClass) {
    const db = EntityDatabase.get(entityClass);
    if (!db)
        throw new Error(`Entity ${entityClass.name} is not registered with a database`);
    return db;
}
function createEntity(Entity, data) {
    const entity = new Entity();
    Object.assign(entity, data);
    const metadata = (0, metadata_1.getEntityMetadata)(Entity);
    for (const column of metadata.columns) {
        if (entity[column.propertyName] === undefined) {
            if (column.autoIncrement)
                entity[column.propertyName] = 0;
            else if (column.nullable)
                entity[column.propertyName] = null;
        }
    }
    return entity;
}
function createEntities(Entity, data) {
    return data.map(item => createEntity(Entity, item));
}
function createQueuedEntity(Entity, data, session) {
    const entity = createEntity(Entity, data);
    session.add(entity);
    return entity;
}
function createQueuedEntities(Entity, data, session) {
    const entities = createEntities(Entity, data);
    session.add(...entities);
    return entities;
}
async function createPersistedEntity(Entity, data, session) {
    const entity = createEntity(Entity, data);
    await persistEntity(entity, session);
    return entity;
}
async function persistEntity(entity, session) {
    await persistEntities([entity], session);
}
async function persistEntities(entities, session) {
    if (session) {
        session.add(...entities);
        await session.flush();
    }
    else {
        for (const entity of entities)
            await getEntityDatabase(entity.constructor).saveEntity(entity);
    }
}
async function createPersistedEntities(Entity, data, session) {
    const entities = createEntities(Entity, data);
    await persistEntities(entities, session);
    return entities;
}
function markEntityClean(entity) {
    OriginalSnapshot.set(entity, cloneRecord(getEntityFields(entity)));
}
function getEntityOriginal(entity) {
    return cloneRecord(OriginalSnapshot.get(entity) ?? {});
}
function hasEntitySnapshot(entity) {
    return OriginalSnapshot.has(entity);
}
function getDirtyDetails(entity) {
    const original = (OriginalSnapshot.get(entity) ?? {});
    const current = getEntityFields(entity);
    const result = {};
    for (const [key, value] of Object.entries(current)) {
        if (!(0, node_util_1.isDeepStrictEqual)(original[key], value))
            result[key] = { original: cloneValue(original[key]), current: value };
    }
    return result;
}
function getDirtyFields(entity) {
    return Object.keys(getDirtyDetails(entity));
}
function isEntityDirty(entity) {
    return getDirtyFields(entity).length > 0;
}
function isFieldDirty(entity, field) {
    return getDirtyFields(entity).includes(field);
}
function getFieldOriginal(entity, field) {
    return getEntityOriginal(entity)[field];
}
function revertDirtyEntity(entity) {
    Object.assign(entity, cloneRecord((OriginalSnapshot.get(entity) ?? {})));
}
function getEntityFields(entity) {
    const metadata = (0, metadata_1.getEntityMetadata)(entity.constructor);
    return Object.fromEntries(metadata.columns.map(column => [column.propertyName, entity[column.propertyName]]));
}
function getPKFieldForEntity(Entity) {
    return (0, metadata_1.getEntityMetadata)(Entity).primaryKey.propertyName;
}
function getPKFieldForEntityInstance(entity) {
    return getPKFieldForEntity(entity.constructor);
}
async function getKeyedEntities(options) {
    const { keyField, entities } = await getEntitiesByIdWithKeyName(options);
    return keyEntities(entities, keyField);
}
async function getKeyedGroupedEntities(options) {
    const { keyField, entities } = await getEntitiesByIdWithKeyName(options);
    const grouped = {};
    for (const entity of entities) {
        const key = String(entity[String(keyField)]);
        const current = Object.prototype.hasOwnProperty.call(grouped, key) ? grouped[key] : undefined;
        if (current)
            current.push(entity);
        else
            defineRecordValue(grouped, key, [entity]);
    }
    return grouped;
}
async function getEntitiesById(options) {
    const { entities } = await getEntitiesByIdWithKeyName(options);
    return entities;
}
async function getEntitiesByIdWithKeyName({ schema, ids, keyField, fields, filter, txn }) {
    const resolvedKeyField = keyField ?? getPKFieldForEntity(schema);
    const resolvedIds = uniqueCompact(ids);
    if (!resolvedIds.length)
        return { keyField: resolvedKeyField, entities: [] };
    const query = txn ? txn.query(schema) : getEntityDatabase(schema).query(schema);
    const idFilter = { [String(resolvedKeyField)]: { $in: resolvedIds } };
    query.filter(filter ? { $and: [idFilter, filter] } : idFilter);
    if (fields?.length)
        query.select(...fields.map(String));
    const entities = (await query.find());
    return { keyField: resolvedKeyField, entities };
}
async function resolveRelated(options) {
    const { src, srcIdField, targetField, targetSchema, targetFields } = options;
    if (!src.length)
        return [];
    const subentitiesById = await getKeyedEntities({
        ids: src.map(entity => entity[srcIdField]),
        schema: targetSchema,
        fields: targetFields,
        txn: options.txn
    });
    for (const entity of src) {
        entity[targetField] = getOwnRecordValue(subentitiesById, String(entity[srcIdField]));
    }
    return src;
}
async function resolveRelatedByPivot(options) {
    const { src, srcIdField, pivotSchema, pivotIdKey, pivotRelatedKey, pivotFilter, targetField, targetSchema, targetFields } = options;
    if (!src.length)
        return [];
    const sourcePkField = srcIdField ?? getPKFieldForEntityInstance(src[0]);
    const pivotEntitiesBySourceId = await getKeyedGroupedEntities({
        ids: src.map(entity => entity[String(sourcePkField)]),
        schema: pivotSchema,
        keyField: pivotIdKey,
        filter: pivotFilter,
        txn: options.txn
    });
    const relatedEntitiesById = await getKeyedEntities({
        ids: Object.values(pivotEntitiesBySourceId).flatMap(pivots => pivots.map(pivot => pivot[String(pivotRelatedKey)])),
        schema: targetSchema,
        fields: targetFields,
        txn: options.txn
    });
    for (const entity of src) {
        const sourceId = String(entity[String(sourcePkField)]);
        entity[targetField] =
            getOwnRecordValue(pivotEntitiesBySourceId, sourceId)?.map(pivot => ({
                ...getOwnRecordValue(relatedEntitiesById, String(pivot[String(pivotRelatedKey)])),
                pivot
            })) ?? [];
    }
    return src;
}
async function resolveRelatedByPivotForOne(options) {
    return (await resolveRelatedByPivot({
        ...options,
        src: [options.src]
    }))[0];
}
async function getEntityOrUndefined(Entity, filter) {
    return getEntityDatabase(Entity).query(Entity).filter(normalizeEntityFilter(Entity, filter)).findOneOrUndefined();
}
async function getEntity(Entity, filter) {
    const entity = await getEntityOrUndefined(Entity, filter);
    if (!entity)
        throw new ItemNotFound();
    return entity;
}
async function getEntityOr404(Entity, filter) {
    const entity = await getEntityOrUndefined(Entity, filter);
    if (!entity)
        throw new errors_1.HttpNotFoundError();
    return entity;
}
async function entityExists(Entity, filter) {
    return getEntityDatabase(Entity).query(Entity).filter(normalizeEntityFilter(Entity, filter)).has();
}
function normalizeEntityFilter(Entity, filter) {
    if (typeof filter === 'object')
        return filter;
    return { [getPKFieldForEntity(Entity)]: filter };
}
function uniqueCompact(values) {
    return [...new Set(values.filter(value => value === 0 || value === 0n || Boolean(value)))];
}
function keyEntities(entities, keyField) {
    const keyed = {};
    for (const entity of entities)
        defineRecordValue(keyed, String(entity[String(keyField)]), entity);
    return keyed;
}
function getOwnRecordValue(record, key) {
    return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
}
function defineRecordValue(record, key, value) {
    Object.defineProperty(record, key, {
        value,
        enumerable: true,
        configurable: true,
        writable: true
    });
}
function cloneRecord(record) {
    return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, cloneValue(value)]));
}
function cloneValue(value) {
    if (value === null || value === undefined)
        return value;
    try {
        return structuredClone(value);
    }
    catch {
        if (value instanceof Date)
            return new Date(value.getTime());
        if (Array.isArray(value))
            return value.map(item => cloneValue(item));
        if (typeof value === 'object')
            return { ...value };
        return value;
    }
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"DataTypes\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":7},{\"kind\":8},{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":0}},{\"kind\":17},{\"kind\":5}],\"typeName\":\"DataTypes\"}}]", [() => (typeof Date !== "undefined" ? Date : (typeof exports !== "undefined" ? exports.Date : undefined))], __tsf_metadata_type);
//# sourceMappingURL=entity.js.map