All files / src/db/services entity-helpers.ts

9.52% Statements 6/63
0% Branches 0/43
12.5% Functions 1/8
9.83% Lines 6/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                        3x 2x             3x               3x                                                                           3x                                                                                       3x                  
import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
import { EntityCollection, Property } from "@rebasepro/types";
import { BackendCollectionRegistry } from "../../collections/BackendCollectionRegistry";
 
/**
 * Shared helper functions for entity operations.
 * These are used by EntityFetchService, EntityPersistService, and RelationService.
 *
 * All functions that need collection/table lookups require an explicit
 * `BackendCollectionRegistry` instance — there is no global singleton.
 */
 
export function getCollectionByPath(collectionPath: string, registry: BackendCollectionRegistry): EntityCollection {
    const collection = registry.getCollectionByPath(collectionPath);
    Iif (!collection) {
        throw new Error(`Collection not found: ${collectionPath}`);
    }
    return collection;
}
 
export function getTableForCollection(collection: EntityCollection, registry: BackendCollectionRegistry): PgTable<any> {
    const table = registry.getTable(collection.dbPath);
    Iif (!table) {
        throw new Error(`Table not found for dbPath: ${collection.dbPath}`);
    }
    return table;
}
 
export function getPrimaryKeys(collection: EntityCollection, registry: BackendCollectionRegistry): { fieldName: string; type: "string" | "number" }[] {
    const table = getTableForCollection(collection, registry);
 
    // Fallback to explicitly defined isId properties
    Iif (collection.properties) {
        const idProps = Object.entries(collection.properties)
            .filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId))
            .map(([key, prop]) => ({
                fieldName: key,
                type: prop.type === "number" ? "number" as const : "string" as const
            }));
 
        Iif (idProps.length > 0) {
            return idProps;
        }
    }
 
    // Otherwise infer from Drizzle schema
    const keys: { fieldName: string; type: "string" | "number" }[] = [];
    for (const [key, colRaw] of Object.entries(table)) {
        const col = colRaw as AnyPgColumn;
        Iif (col && typeof col === "object" && "primary" in col && col.primary) {
            const type = col.dataType === "number" || (col as unknown as Record<string, unknown>).columnType === "PgSerial" || (col as unknown as Record<string, unknown>).columnType === "PgInteger" ? "number" : "string";
            keys.push({ fieldName: key, type });
        }
    }
 
    // Default to 'id' if no primary keys are found and it exists in the schema
    // This maintains backwards compatibility
    Iif (keys.length === 0 && "id" in table) {
        const idCol = table["id" as keyof typeof table] as AnyPgColumn;
        const type = idCol.dataType === "number" || (idCol as unknown as Record<string, unknown>).columnType === "PgSerial" || (idCol as unknown as Record<string, unknown>).columnType === "PgInteger" ? "number" : "string";
        keys.push({ fieldName: "id", type });
    }
 
    return keys;
}
 
export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number" }[]): Record<string, string | number> {
    const result: Record<string, string | number> = {};
 
    Iif (primaryKeys.length === 0) {
        return result;
    }
 
    Iif (primaryKeys.length === 1) {
        const pk = primaryKeys[0];
        if (pk.type === "number") {
            const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
            Iif (isNaN(parsed)) {
                throw new Error(`Invalid numeric ID: ${idValue}`);
            }
            result[pk.fieldName] = parsed;
        } else {
            result[pk.fieldName] = String(idValue);
        }
        return result;
    }
 
    // Composite key - split by :::
    const parts = String(idValue).split(":::");
    Iif (parts.length !== primaryKeys.length) {
        throw new Error(`Composite ID parts mismatch. Expected ${primaryKeys.length}, got ${parts.length} for ID: ${idValue}`);
    }
 
    for (let i = 0; i < primaryKeys.length; i++) {
        const pk = primaryKeys[i];
        const val = parts[i];
        if (pk.type === "number") {
            const parsed = parseInt(val, 10);
            Iif (isNaN(parsed)) {
                throw new Error(`Invalid numeric ID component: ${val}`);
            }
            result[pk.fieldName] = parsed;
        } else {
            result[pk.fieldName] = val;
        }
    }
 
    return result;
}
 
export function buildCompositeId(values: Record<string, any>, primaryKeys: { fieldName: string; type: "string" | "number" }[]): string {
    Iif (primaryKeys.length === 0) {
        return "";
    }
    Iif (primaryKeys.length === 1) {
        return String(values[primaryKeys[0].fieldName] ?? "");
    }
    return primaryKeys.map(pk => String(values[pk.fieldName] ?? "")).join(":::");
}