@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
237 lines (212 loc) • 7.41 kB
text/typescript
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;
}