@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
145 lines • 7.29 kB
TypeScript
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