UNPKG

@clickup/ent-framework

Version:

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

237 lines (212 loc) 7.41 kB
import compact from "lodash/compact"; import type { Client } from "../../abstract/Client"; import type { Cluster } from "../../abstract/Cluster"; import type { Schema } from "../../abstract/Schema"; import { entries } from "../../internal/misc"; import type { FieldOfIDType, Table, UniqueKey } from "../../types"; import { ID } from "../../types"; import { Configuration } from "../Configuration"; import { Inverse } from "../Inverse"; import { GLOBAL_SHARD, type ShardAffinity } from "../ShardAffinity"; import { ShardLocator } from "../ShardLocator"; import { Triggers } from "../Triggers"; import { Validation } from "../Validation"; export interface ConfigInstance {} export interface ConfigClass< TTable extends Table, TUniqueKey extends UniqueKey<TTable>, TClient extends Client, > { /** * Some Ent parameters need to be configured lazily, on the 1st access, * because there could be cyclic references between Ent classes (e.g. in their * privacy rules). So configure() is called on some later stage, at the moment * of actual Ent operations (like loading, creation etc.). There is no static * abstract methods in TS yet, so making it non-abstract. */ configure(): Configuration<TTable>; /** * A helper class to work-around TS weakness in return value type inference: * https://github.com/Microsoft/TypeScript/issues/31273. It could've been just * a function, but having a class is a little more natural. */ readonly Configuration: new ( cfg: Configuration<TTable>, ) => Configuration<TTable>; /** * A Cluster where this Ent lives. */ readonly CLUSTER: Cluster<TClient>; /** * A schema which represents this Ent. */ readonly SCHEMA: Schema<TTable, TUniqueKey>; /** * Defines how to find the right Shard during Ent insertion. */ readonly SHARD_AFFINITY: ShardAffinity<FieldOfIDType<TTable>>; /** * Shard locator for this Ent, responsible for resolving IDs into Shard objects. */ readonly SHARD_LOCATOR: ShardLocator<TClient, TTable, FieldOfIDType<TTable>>; /** * Privacy rules for this Ent class. */ readonly VALIDATION: Validation<TTable>; /** * Triggers for this Ent class. */ readonly TRIGGERS: Triggers<TTable>; /** * Inverse assoc managers for fields. */ readonly INVERSES: Array<Inverse<TClient, TTable>>; /** * TS requires us to have a public constructor to infer instance types in * various places. We make this constructor throw if it's called. */ new (): ConfigInstance; } /** * Modifies the passed class adding support for Ent configuration (such as: * Cluster, table schema, privacy rules, triggers etc.). */ export function ConfigMixin< TTable extends Table, TUniqueKey extends UniqueKey<TTable>, TClient extends Client, >( Base: new () => {}, cluster: Cluster<TClient>, schema: Schema<TTable, TUniqueKey>, ): ConfigClass<TTable, TUniqueKey, TClient> { class ConfigMixin extends Base { static Configuration: new ( c: Configuration<TTable>, ) => Configuration<TTable> = Configuration; static readonly CLUSTER = cluster; static readonly SCHEMA = schema; static get SHARD_AFFINITY(): ShardAffinity<FieldOfIDType<TTable>> { Object.defineProperty(this, "SHARD_AFFINITY", { value: this.configure().shardAffinity, writable: false, }); return this.SHARD_AFFINITY; } static get SHARD_LOCATOR(): ShardLocator< TClient, TTable, FieldOfIDType<TTable> > { Object.defineProperty(this, "SHARD_LOCATOR", { value: new ShardLocator({ cluster, entName: this.name, shardAffinity: this.SHARD_AFFINITY, uniqueKey: schema.uniqueKey, inverses: this.INVERSES, }), writable: false, }); return this.SHARD_LOCATOR; } static get VALIDATION(): Validation<TTable> { const cfg = this.configure(); Object.defineProperty(this, "VALIDATION", { value: new Validation(this.name, { tenantPrincipalField: cfg.privacyTenantPrincipalField, inferPrincipal: async (vc, row) => { const res = typeof cfg.privacyInferPrincipal === "function" ? await cfg.privacyInferPrincipal(vc, row) : cfg.privacyInferPrincipal; const lowerVC = typeof res === "string" ? vc.toLowerInternal(res) : res === null ? vc.toGuest() : res.vc; if (lowerVC.isOmni()) { throw Error( `It is prohibited to return an omni VC "${lowerVC.toString()}" from ${this.name} privacyInferPrincipal callback. Loading VC was: "${vc.toString()}".`, ); } else { return lowerVC; } }, load: cfg.privacyLoad, insert: cfg.privacyInsert, update: cfg.privacyUpdate, delete: cfg.privacyDelete, validate: cfg.validators, }), writable: false, }); return this.VALIDATION; } static get TRIGGERS(): Triggers<TTable> { const cfg = this.configure(); Object.defineProperty(this, "TRIGGERS", { value: new Triggers( cfg.beforeInsert ?? [], (cfg.beforeUpdate ?? []).map((trigger) => trigger instanceof Array ? trigger : [null, trigger], ), cfg.beforeDelete ?? [], (cfg.beforeMutation ?? []).map((trigger) => trigger instanceof Array ? trigger : [null, trigger], ), cfg.afterInsert ?? [], (cfg.afterUpdate ?? []).map((trigger) => trigger instanceof Array ? trigger : [null, trigger], ), cfg.afterDelete ?? [], (cfg.afterMutation ?? []).map((trigger) => trigger instanceof Array ? trigger : [null, trigger], ), ), writable: false, }); return this.TRIGGERS; } static get INVERSES(): Array<Inverse<TClient, TTable>> { const cfg = this.configure(); Object.defineProperty(this, "INVERSES", { value: compact( entries(cfg.inverses ?? {}).map(([field, { name, type }]) => { if (this.SHARD_AFFINITY === GLOBAL_SHARD) { throw Error( `It's useless to define a ${field} inverse for GLOBAL_SHARD schemas; use just a DB index`, ); } const spec = schema.table[field]; if ( schema.table[field].type !== ID || spec.autoInsert || spec.autoUpdate ) { throw Error( `To have inverse specified, the '${field}' must be of type ${ID} and have no autoInsert/autoUpdate`, ); } return new Inverse({ cluster, shardAffinity: this.SHARD_AFFINITY as ShardAffinity<string>, id2Schema: schema, id2Field: field, name, type, }); }), ), writable: false, }); return this.INVERSES; } override ["constructor"]!: typeof ConfigMixin; static configure(): Configuration<TTable> { throw Error(`Please define ${this.name}.configure() method`); } } return ConfigMixin; }