@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
86 lines (78 loc) • 3.02 kB
text/typescript
import { localUniqueInt, nullthrows } from "../../internal/misc";
import type { Row, RowWithID, Table, Where } from "../../types";
import { ID } from "../../types";
import type { EntClass } from "../types";
import type { VC } from "../VC";
import type { Predicate } from "./Predicate";
import { IDsCacheCanReadIncomingEdge } from "./Predicate";
/**
* An ent may represent not necessarily a node in the graph, but also an edge
* between two nodes. Consider EntMember in the below example:
*
* vc.principal <--- EntMember[user_id, company_id] ---> EntCompany
*
* This predicate verifies that for a e.g. given EntCompany row and a given VC,
* an EntMember row exists (and optionally matches some criterion) in the
* database.
*
* - entEdgeVCField = user_id in the above example
* - entEdgeFKField = company_id in the above example
* - if an EntMember object exists, it must also match entEdgeFilter()
*/
export class IncomingEdgeFromVCExists<TEdgeTable extends Table>
implements Predicate<RowWithID>
{
private readonly instanceID = localUniqueInt();
readonly name;
constructor(
public readonly EntEdge: EntClass<TEdgeTable>,
public readonly entEdgeVCField: keyof Row<TEdgeTable>,
public readonly entEdgeFKField: keyof Row<TEdgeTable>,
public readonly entEdgeFilter?: (ent: Row<TEdgeTable>) => boolean,
) {
this.name =
this.constructor.name +
"(" +
this.EntEdge.name +
"[" +
`${this.entEdgeVCField}=vc, ` +
`${this.entEdgeFKField}=row.${ID}` +
"]" +
")";
}
async check(vc: VC, row: RowWithID): Promise<boolean> {
const cache = vc.cache(IDsCacheCanReadIncomingEdge);
const cacheKey = nullthrows(row[ID]) + ":" + this.instanceID;
if (cache.has(cacheKey)) {
return true;
}
const where = {
[this.entEdgeFKField]: row[ID],
[this.entEdgeVCField]: vc.principal,
} as Where<TEdgeTable>;
let allow: boolean;
if (this.entEdgeFilter) {
// We use an omni VC here to avoid cyclic references where the edge ent
// delegates permission checks to the row ent, and row ent loads the edge
// ent to run the edgeEntFilter function. It's safe, because:
// 1. Omni VC is always demoted to the current user's VC (which is vc
// since we filter by vc.principal above) or to a guest VC if it cannot
// find a user_id field in the edge ent (which also never happens).
// 2. The edgeEntFilter function is synchronous, so it can't physically
// access the database anyway.
const ents = await this.EntEdge.select(vc.toOmniDangerous(), where, 1);
const filtered = ents.filter((ent) =>
this.entEdgeFilter!(ent as Row<TEdgeTable>),
);
allow = filtered.length > 0;
} else {
// Exists is not privacy-checked (it doesn't fetch any row to be checked).
allow = await this.EntEdge.exists(vc, where);
}
if (allow) {
cache.add(cacheKey);
return true;
}
return false;
}
}