import type { Flatten, Writeable } from "../internal/misc";
import type { InsertInput, Row, RowWithID, Table, UpdateInput, Value } from "../types";
import type { VC } from "./VC";
/**
 * Table -> trigger's before- and after-insert input. Below, we use InsertInput
 * and not Row, because before and even after some INSERT, we may still not know
 * some values of the row (they can be filled by the DB in e.g. autoInsert
 * clause). InsertInput is almost a subset of Row, but it has stricter symbol
 * keys: e.g. if some symbol key is non-optional in INSERT (aka doesn't have
 * autoInsert), it will always be required in InsertInput too.
 */
export type TriggerInsertInput<TTable extends Table> = Flatten<InsertInput<TTable> & RowWithID>;
/**
 * Table -> trigger's before-update input.
 */
export type TriggerUpdateInput<TTable extends Table> = Flatten<UpdateInput<TTable>>;
/**
 * Table -> trigger's before- and after-update NEW row. Ephemeral (symbol)
 * fields may or may not be passed depending on what the user passes to the
 * update method.
 */
export type TriggerUpdateNewRow<TTable extends Table> = Flatten<Readonly<Row<TTable> & {
    [K in keyof TTable & symbol]?: Value<TTable[K]> | undefined;
}>>;
/**
 * Table -> trigger's before- and after-update (or delete) OLD row. Ephemeral
 * (symbol) fields are marked as always presented, but "never" typed, so they
 * will be available for dereferencing in newOrOldRow of before/after mutation
 * triggers without guard-checking of op value.
 */
export type TriggerUpdateOrDeleteOldRow<TTable extends Table> = Flatten<Readonly<Row<TTable> & Record<keyof TTable & symbol, never>>>;
/**
 * Triggers could be used to simulate "transactional best-effort behavior" in a
 * non-transactional combination of some services. Imagine we have a relational
 * database and a queue service; each time we change something in the query, we
 * want to schedule the ID to the queue. Queue service is faulty: if a queueing
 * operation fails, we don't want the data to be stored to the DB afterwards.
 * Queries are faulty too, but it's okay for us to have something added to the
 * queue even if the corresponding query failed after it (a queue worker will
 * just do a no-op since it anyway rechecks the source of truth in relational
 * DBs). Queue service is like a write-ahead log for DB which always has
 * not-less records than the DB. In this case, we have the following set of
 * triggers:
 *
 * 1. beforeInsert: schedules ID to the queue (ID is known, see below why)
 * 2. beforeUpdate: schedules ID to the queue
 * 3. afterDelete: optionally schedule ID removal to the queue (notice "after")
 *
 * Notice that ID is always known in all cases, even in insertBefore triggers,
 * because we split an INSERT operation into gen_id+insert parts, and the
 * triggers are executed in between.
 *
 * Triggers are invoked sequentially. Any exception thrown in a before-trigger
 * is propagated to the caller, and the DB operation is skipped.
 *
 * Triggers for beforeInsert and beforeUpdate can change their input parameter,
 * the change will apply to the database.
 *
 * Naming convention for trigger arguments:
 * 1. input: whatever is passed to the operation. Notice that due to us having
 *    autoInsert/autoUpdate fields, the set of fields can be incomplete here!
 * 1. oldRow: the entire row in the DB which was there before the operation. All
 *    the fields will be presented there.
 * 2. newRow: a row in the DB as it will looks like after the operation. Notice
 *    that it can be non precise, because we don't always reload the updated row
 *    from the database! What we do is just field by field application of input
 *    properties to oldRow.
 */
export type InsertTrigger<TTable extends Table> = (vc: VC, args: {
    input: TriggerInsertInput<TTable>;
}) => Promise<unknown> | unknown;
export type BeforeUpdateTrigger<TTable extends Table> = (vc: VC, args: {
    newRow: TriggerUpdateNewRow<TTable>;
    oldRow: TriggerUpdateOrDeleteOldRow<TTable>;
    input: TriggerUpdateInput<TTable>;
}) => Promise<unknown> | unknown;
export type AfterUpdateTrigger<TTable extends Table> = (vc: VC, args: {
    newRow: TriggerUpdateNewRow<TTable>;
    oldRow: TriggerUpdateOrDeleteOldRow<TTable>;
}) => Promise<unknown> | unknown;
export type DeleteTrigger<TTable extends Table> = (vc: VC, args: {
    oldRow: TriggerUpdateOrDeleteOldRow<TTable>;
}) => Promise<unknown> | unknown;
export type BeforeMutationTrigger<TTable extends Table> = (vc: VC, args: {
    op: "INSERT";
    newOrOldRow: Readonly<TriggerInsertInput<TTable>>;
    input: TriggerInsertInput<TTable>;
} | {
    op: "UPDATE";
    newOrOldRow: TriggerUpdateNewRow<TTable>;
    input: TriggerUpdateInput<TTable>;
} | {
    op: "DELETE";
    newOrOldRow: TriggerUpdateOrDeleteOldRow<TTable>;
    /** We allow people to modify input of a DELETE operation, although it
     * will be a no-op. This is for convenience: if we remained it
     * read-only, then people would need to check `if (op !== "DELETE") ...`
     * in their beforeMutation triggers, which is a boilerplate. */
    input: Writeable<TriggerUpdateOrDeleteOldRow<TTable>>;
}) => Promise<unknown> | unknown;
export type AfterMutationTrigger<TTable extends Table> = (vc: VC, args: {
    op: "INSERT";
    newOrOldRow: Readonly<TriggerInsertInput<TTable>>;
} | {
    op: "UPDATE";
    newOrOldRow: TriggerUpdateNewRow<TTable>;
} | {
    op: "DELETE";
    newOrOldRow: TriggerUpdateOrDeleteOldRow<TTable>;
}) => Promise<unknown> | unknown;
export type DepsBuilder<TTable extends Table> = (vc: VC, row: Flatten<Readonly<Row<TTable>>>) => unknown[] | Promise<unknown[]>;
export declare class Triggers<TTable extends Table> {
    private beforeInsert;
    private beforeUpdate;
    private beforeDelete;
    private beforeMutation;
    private afterInsert;
    private afterUpdate;
    private afterDelete;
    private afterMutation;
    constructor(beforeInsert: Array<InsertTrigger<TTable>>, beforeUpdate: Array<[
        DepsBuilder<TTable> | null,
        BeforeUpdateTrigger<TTable>
    ]>, beforeDelete: Array<DeleteTrigger<TTable>>, beforeMutation: Array<[
        DepsBuilder<TTable> | null,
        BeforeMutationTrigger<TTable>
    ]>, afterInsert: Array<InsertTrigger<TTable>>, afterUpdate: Array<[
        DepsBuilder<TTable> | null,
        AfterUpdateTrigger<TTable>
    ]>, afterDelete: Array<DeleteTrigger<TTable>>, afterMutation: Array<[
        DepsBuilder<TTable> | null,
        AfterMutationTrigger<TTable>
    ]>);
    hasInsertTriggers(): boolean;
    hasUpdateTriggers(): boolean;
    wrapInsert(func: (input: InsertInput<TTable> & RowWithID) => Promise<string | null>, vc: VC, input: InsertInput<TTable> & RowWithID): Promise<string | null>;
    wrapUpdate(func: (input: UpdateInput<TTable>) => Promise<boolean>, vc: VC, oldRow: TriggerUpdateOrDeleteOldRow<TTable>, input: UpdateInput<TTable>): Promise<boolean>;
    wrapDelete(func: () => Promise<boolean>, vc: VC, oldRow: TriggerUpdateOrDeleteOldRow<TTable>): Promise<boolean>;
}
/**
 * Simulates an update for a row, as if it's applied to the Ent.
 * @ignore
 */
export declare function buildUpdateNewRow<TTable extends Table>(oldRow: Row<TTable>, input: UpdateInput<TTable>): TriggerUpdateNewRow<TTable>;
//# sourceMappingURL=Triggers.d.ts.map