graplix
Version:
Authorization framework for implementing Relation-based Access Control (ReBAC) with the Resolver (Inspired by [GraphQL](https://graphql.org))
217 lines (190 loc) • 5.22 kB
text/typescript
import { DirectedGraph } from "graphology";
import * as R from "remeda";
import type { BaseEntityTypeMap } from "./BaseEntityTypeMap";
import type { GraplixInput } from "./GraplixInput";
import type { ValueOf } from "./utils";
import {
assignGraph,
compileNodeId,
isEqual,
normalizeArrayable,
} from "./utils";
export type QueryParameters<
Context extends {},
EntityTypeMap extends BaseEntityTypeMap,
> = {
user: ValueOf<EntityTypeMap>;
object: ValueOf<EntityTypeMap>;
relation: string;
context: Context;
};
export type QueryOptions<
Context extends {},
EntityTypeMap extends BaseEntityTypeMap,
> = {
initialParams?: QueryParameters<Context, EntityTypeMap>;
implicit?: boolean;
};
export async function query<
Context extends {},
EntityTypeMap extends BaseEntityTypeMap,
>(
input: GraplixInput<Context, EntityTypeMap>,
params: QueryParameters<Context, EntityTypeMap>,
options?: QueryOptions<Context, EntityTypeMap>,
): Promise<
DirectedGraph<{ node: any; matched: boolean }, { relation: string }>
> {
const object = params.object;
const objectType = object.$type;
const objectId = input.resolvers[objectType].identify(params.object);
const objectNodeId = compileNodeId({
type: objectType,
id: objectId,
});
const user = params.user;
const userId = input.resolvers[user.$type].identify(user);
const userNodeId = compileNodeId({
type: user.$type,
id: userId,
});
const graph = new DirectedGraph<
{ node: any; matched: boolean },
{ relation: string }
>();
graph.addNode(objectNodeId, {
node: object,
matched: !options?.implicit && objectNodeId === userNodeId,
});
const isCircularDependency =
options?.initialParams &&
isEqual(
(t) =>
compileNodeId({
type: t.$type,
id: input.resolvers[t.$type].identify(t),
}),
params.object,
options.initialParams.object,
) &&
params.relation === options.initialParams.relation;
if (isCircularDependency) {
return graph;
}
const initialParams = options?.initialParams ?? params;
const relationDefinitions = normalizeArrayable(
input.schema[objectType][params.relation],
);
const resolverDefinitions = normalizeArrayable(
input.resolvers[objectType].relations?.[params.relation],
);
/**
* { type: "..." }
*/
await R.pipe(
relationDefinitions,
R.filter(
(def): def is Extract<typeof def, { type: string }> => "type" in def,
),
R.map(async (relationDefinition) => {
const resolverDefinition = resolverDefinitions.find(
(d) => d.type === relationDefinition.type,
);
if (!resolverDefinition?.resolve) {
return;
}
const nextNodes = await resolverDefinition
.resolve(params.object, params.context)
.then((o) => normalizeArrayable(o));
for (const nextNode of nextNodes) {
const nextNodeId = compileNodeId({
type: nextNode.$type,
id: input.resolvers[nextNode.$type].identify(nextNode),
});
/**
* - if implicit relation, return false
* - if not implicit relation and nextNodeId is equal to userNodeId, return true
*/
const matched = !options?.implicit && nextNodeId === userNodeId;
try {
graph.dropNode(nextNodeId);
} catch (e) {}
graph.addNode(nextNodeId, {
node: nextNode,
matched,
});
graph.addDirectedEdge(objectNodeId, nextNodeId, {
relation: params.relation,
});
}
}),
(promises) => Promise.all(promises),
);
/**
* { when: "...", ?? }
*/
await R.pipe(
relationDefinitions,
R.filter(
(def): def is Extract<typeof def, { when: string }> => "when" in def,
),
R.map(async (relationDefinition) => {
/**
* { when: "..." }
*/
if (!relationDefinition.from) {
const g1 = await query(
input,
{
...params,
relation: relationDefinition.when,
},
{
initialParams,
},
);
assignGraph(graph, g1);
}
/**
* { when: "...", from: "..." }
*/
if (relationDefinition.from) {
const g2 = await query(
input,
{
...params,
relation: relationDefinition.from,
},
{
initialParams,
implicit: true,
},
);
assignGraph(graph, g2);
await R.pipe(
Array.from(g2.nodeEntries()),
R.map(async ({ node: nodeId, attributes: { node } }) => {
if (nodeId === objectNodeId) {
return;
}
const g3 = await query(
input,
{
...params,
object: node,
relation: relationDefinition.when,
},
{
initialParams,
},
);
assignGraph(graph, g3);
}),
(promises) => Promise.all(promises),
);
}
}),
(promises) => Promise.all(promises),
);
return graph;
}