UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

145 lines 7.29 kB
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