UNPKG

@clickup/ent-framework

Version:

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

145 lines (141 loc) 7.02 kB
import type { FieldOfIDType, FieldOfIDTypeRequired, Row, Table, } from "../types"; import type { ShardAffinity } from "./ShardAffinity"; import type { AfterMutationTrigger, AfterUpdateTrigger, BeforeMutationTrigger, BeforeUpdateTrigger, DeleteTrigger, DepsBuilder, InsertTrigger, } from "./Triggers"; import type { Ent } from "./types"; import type { Validation, ValidationRules } from "./Validation"; import type { VC } from "./VC"; /** * Strongly typed configuration framework to force TS auto-infer privacy * callbacks arguments types (which are not Ents, but row-like inputs). * * Motivation: * 1. We MUST resolve privacyXyz rules below lazily, at actual operation; * otherwise in case of cyclic Ent dependencies between EntA and EntB, one of * them will be magically undefined. * 2. We can’t define these parameter as BaseEnt arguments: privacy rules may * refer the derived Ent itself and other Ents and thus produce cyclic * dependencies. TS doesn't allow to work with such cyclic dependencies * during the class is defining. * 3. Configuration can’t be just returned from a virtual method, because in TS, * type inference in return values is poor: * https://github.com/Microsoft/TypeScript/issues/31273 */ export class Configuration<TTable extends Table> { /** Defines how to locate a Shard at Ent insert time. See ShardAffinity for * more details. * * 1. GLOBAL_SHARD: places the Ent in the global Shard (0). * 2. `[]`: places the Ent in a random Shard. The "randomness" of the "random * Shard" is deterministic by the Ent's unique key at the moment of * insertion (if it's defined; otherwise completely random). This helps two * racy insert operations running concurrently to choose the same Shard for * the Ent to be created in, so only one of them will win, instead of both * winning and mistakenly creating the Ent duplicates. I.e. having the same * value in unique key forces the engine to target the same "random" Shard. * 3. `["field1", "field2", ...]`: places the Ent in the Shard that is pointed * to by the value in field1 (if it's null, then field2 etc.). * * A special treatment is applied if a fieldN value in (3) points to the * global Shard. In such a case, the Shard for the current Ent is chosen * deterministic-randomly at insert time, as if [] is passed. This allows the * Ent to refer other "owning" Ents of different types, some of which may be * located in the global Shard. Keep in mind that, to locate such an Ent * pointing to another Ent in the global Shard, an inverse for fieldN must be * defined in most of the cases. */ readonly shardAffinity!: ShardAffinity<FieldOfIDType<TTable>>; /** Inverses allow cross-Shard foreign keys & cross-Shard selection. If a * field points to an Ent in another Shard, and we're e.g. selecting by a * value in this field, inverses allow to locate Shard(s) of the Ent. */ readonly inverses?: { [k in FieldOfIDTypeRequired<TTable>]?: { name: string; type: string }; }; /** If defined, forces all Ents of this class to have the value of that field * equal to VC's principal at load time. This is a very 1st unavoidable check * in the privacy rules chain, thus it's bullet-proof. */ readonly privacyTenantPrincipalField?: Validation<TTable>["tenantPrincipalField"]; /** An attempt to load this Ent using an omni VC will "lower" that VC to the * principal returned. Omni VC is always lowered. * 1. If an Ent is returned, the lowered principal will be Ent#vc.principal. * It is a way to delegate principal inference to another Ent. * 2. If a string is returned, then it's treated as a principal ID. * 3. If a null is returned, then a guest principal will be used. * 4. Returning an omni principal or VC will result in a run-time error. */ readonly privacyInferPrincipal!: | (( vc: VC, row: Row<TTable>, ) => Promise<Ent<{}> | string | null> | string | null) | string | null; /** Privacy rules checked on every row loaded from the DB. */ readonly privacyLoad!: ValidationRules<TTable>["load"]; /** Privacy rules checked before a row is inserted to the DB. * - It the list is empty, then only omni VC can insert; it's typically a good * option for Ents representing e.g. a user. * - If no update/delete rules are defined, then privacyInsert rules are also * run on update/delete by default. * - Unless empty, the rules must include at least one Require() predicate, * they can't entirely consist of AllowIf(). This is because for write rules * (privacyInsert, privacyUpdate, privacyDelete) it's important to make sure * that ALL rules permit the operation, not only one of them allows it; this * is what Require() is exactly for. */ readonly privacyInsert!: ValidationRules<TTable>["insert"]; /** Privacy rules checked before a row is updated in the DB. * - If not defined, privacyInsert rules are used. * - The rules must include at least one Require() predicate. */ readonly privacyUpdate?: ValidationRules<TTable>["update"]; /** Privacy rules checked before a row is deleted in the DB. * - If not defined, privacyInsert rules are used. * - The rules must include at least one Require() predicate. */ readonly privacyDelete?: ValidationRules<TTable>["delete"]; /** Custom field values validators run before any insert/update. */ readonly validators?: ValidationRules<TTable>["validate"]; /** Triggers run before every insert. */ readonly beforeInsert?: Array<InsertTrigger<TTable>>; /** Triggers run before every update. */ readonly beforeUpdate?: Array< | BeforeUpdateTrigger<TTable> | [DepsBuilder<TTable>, BeforeUpdateTrigger<TTable>] >; /** Triggers run before every delete. */ readonly beforeDelete?: Array<DeleteTrigger<TTable>>; /** Triggers run before every insert/update/delete. Each trigger may also be * passed as "React useEffect-like" tuple where the callback is executed only * if the deps are modified. */ readonly beforeMutation?: Array< | BeforeMutationTrigger<TTable> | [DepsBuilder<TTable>, BeforeMutationTrigger<TTable>] >; /** Triggers run after every delete. */ readonly afterInsert?: Array<InsertTrigger<TTable>>; /** Triggers run after every update. */ readonly afterUpdate?: Array< | AfterUpdateTrigger<TTable> | [DepsBuilder<TTable>, AfterUpdateTrigger<TTable>] >; /** Triggers run after every delete. */ readonly afterDelete?: Array<DeleteTrigger<TTable>>; /** Triggers run after every insert/update/delete. Each trigger may also be * passed as "React useEffect-like" tuple where the callback is executed only * if the deps are modified. */ readonly afterMutation?: Array< | AfterMutationTrigger<TTable> | [DepsBuilder<TTable>, AfterMutationTrigger<TTable>] >; constructor(cfg: Configuration<TTable>) { Object.assign(this, cfg); } }