import { assert } from "../../shared/src/asserts.js";
import { formatPgInternalConvert, sql, sqlConvertColumnArg, } from "../../z2s/src/sql.js";
import { getServerSchema } from "./schema.js";
export class TransactionImpl {
    location = 'server';
    reason = 'authoritative';
    dbTransaction;
    clientID;
    mutationID;
    mutate;
    query;
    constructor(dbTransaction, clientID, mutationID, mutate, query) {
        this.dbTransaction = dbTransaction;
        this.clientID = clientID;
        this.mutationID = mutationID;
        this.mutate = mutate;
        this.query = query;
    }
}
const dbTxSymbol = Symbol();
const serverSchemaSymbol = Symbol();
export async function makeServerTransaction(dbTransaction, clientID, mutationID, schema, mutate, query) {
    const serverSchema = await getServerSchema(dbTransaction, schema);
    return new TransactionImpl(dbTransaction, clientID, mutationID, mutate(dbTransaction, serverSchema), query(dbTransaction, serverSchema));
}
export function makeSchemaCRUD(schema) {
    const schemaCRUDs = {};
    for (const tableSchema of Object.values(schema.tables)) {
        schemaCRUDs[tableSchema.name] = makeTableCRUD(tableSchema);
    }
    /**
     * For users with very large schemas it is expensive to re-create
     * all the CRUD mutators for each transaction. Instead, we create
     * them all once up-front and then bind them to the transaction
     * as requested.
     */
    class CRUDHandler {
        #dbTransaction;
        #serverSchema;
        constructor(dbTransaction, serverSchema) {
            this.#dbTransaction = dbTransaction;
            this.#serverSchema = serverSchema;
        }
        get(target, prop) {
            if (prop in target) {
                return target[prop];
            }
            const txHolder = {
                [dbTxSymbol]: this.#dbTransaction,
                [serverSchemaSymbol]: this.#serverSchema,
            };
            target[prop] = Object.fromEntries(Object.entries(schemaCRUDs[prop]).map(([name, method]) => [
                name,
                method.bind(txHolder),
            ]));
            return target[prop];
        }
    }
    return (dbTransaction, serverSchema) => new Proxy({}, new CRUDHandler(dbTransaction, serverSchema));
}
function removeUndefined(value) {
    const valueWithoutUndefined = {};
    for (const [key, val] of Object.entries(value)) {
        if (val !== undefined) {
            valueWithoutUndefined[key] = val;
        }
    }
    return valueWithoutUndefined;
}
function makeTableCRUD(schema) {
    return {
        async insert(value) {
            value = removeUndefined(value);
            const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
            const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
            const stmt = formatPgInternalConvert(sql `INSERT INTO ${sql.ident(serverName(schema))} (${sql.join(targetedColumns.map(([, serverName]) => sql.ident(serverName)), ',')}) VALUES (${sql.join(Object.entries(value).map(([col, v]) => sqlInsertValue(v, serverTableSchema[serverNameFor(col, schema)])), ', ')})`);
            const tx = this[dbTxSymbol];
            await tx.query(stmt.text, stmt.values);
        },
        async upsert(value) {
            value = removeUndefined(value);
            const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
            const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
            const primaryKeyColumns = origAndServerNamesFor(schema.primaryKey, schema);
            const stmt = formatPgInternalConvert(sql `INSERT INTO ${sql.ident(serverName(schema))} (${sql.join(targetedColumns.map(([, serverName]) => sql.ident(serverName)), ',')}) VALUES (${sql.join(Object.entries(value).map(([col, val]) => sqlInsertValue(val, serverTableSchema[serverNameFor(col, schema)])), ', ')}) ON CONFLICT (${sql.join(primaryKeyColumns.map(([, serverName]) => sql.ident(serverName)), ', ')}) DO UPDATE SET ${sql.join(Object.entries(value).map(([col, val]) => sql `${sql.ident(schema.columns[col].serverName ?? col)} = ${sqlInsertValue(val, serverTableSchema[serverNameFor(col, schema)])}`), ', ')}`);
            const tx = this[dbTxSymbol];
            await tx.query(stmt.text, stmt.values);
        },
        async update(value) {
            value = removeUndefined(value);
            const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
            const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
            const stmt = formatPgInternalConvert(sql `UPDATE ${sql.ident(serverName(schema))} SET ${sql.join(targetedColumns.map(([origName, serverName]) => sql `${sql.ident(serverName)} = ${sqlInsertValue(value[origName], serverTableSchema[serverName])}`), ', ')} WHERE ${primaryKeyClause(schema, serverTableSchema, value)}`);
            const tx = this[dbTxSymbol];
            await tx.query(stmt.text, stmt.values);
        },
        async delete(value) {
            value = removeUndefined(value);
            const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
            const stmt = formatPgInternalConvert(sql `DELETE FROM ${sql.ident(serverName(schema))} WHERE ${primaryKeyClause(schema, serverTableSchema, value)}`);
            const tx = this[dbTxSymbol];
            await tx.query(stmt.text, stmt.values);
        },
    };
}
function serverName(x) {
    return x.serverName ?? x.name;
}
function primaryKeyClause(schema, serverTableSchema, row) {
    const primaryKey = origAndServerNamesFor(schema.primaryKey, schema);
    return sql `${sql.join(primaryKey.map(([origName, serverName]) => sql `${sql.ident(serverName)}${maybeCastColumn(serverTableSchema[serverName])} = ${sqlValue(row[origName], serverTableSchema[serverName])}`), ' AND ')}`;
}
function maybeCastColumn(col) {
    if (col.type === 'uuid' || col.isEnum) {
        return sql `::text`;
    }
    return sql ``;
}
function origAndServerNamesFor(originalNames, schema) {
    return originalNames.map(name => [name, serverNameFor(name, schema)]);
}
function serverNameFor(originalName, schema) {
    const col = schema.columns[originalName];
    assert(col, `Column ${originalName} was not found in the Zero schema for the table ${schema.name}`);
    return col.serverName ?? originalName;
}
function sqlValue(value, serverColumnSchema) {
    return sqlConvertColumnArg(serverColumnSchema, value, false, true);
}
function sqlInsertValue(value, serverColumnSchema) {
    return sqlConvertColumnArg(serverColumnSchema, value, false, false);
}
//# sourceMappingURL=custom.js.map