Skip to content

Commit

Permalink
fix actors queries
Browse files Browse the repository at this point in the history
  • Loading branch information
ramonsnir committed Jun 12, 2022
1 parent a215464 commit ccd63cd
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 30 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sunbear",
"version": "0.0.6",
"version": "0.0.7",
"description": "Authorization library for SQL",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down
3 changes: 2 additions & 1 deletion src/Driver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PlannedQuery } from './queryPlan';

export interface Driver<Query, Node> {
emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): Query;
emitGoalsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): Query;
emitActorsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): Query;
run<Goal extends Node>(query: Query): Promise<Goal[]>;
}
8 changes: 4 additions & 4 deletions src/SunBear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class SunBear<
if (solution === true) {
return true;
}
const query = this.driver.emit(planQuery(this.schema, solution));
const query = this.driver.emitGoalsQuery(planQuery(this.schema, solution));
return (await this.driver.run(query)).length > 0;
}

Expand All @@ -60,7 +60,7 @@ export class SunBear<
if (solution === true) {
return { success: true, sync: true };
}
const query = this.driver.emit(planQuery(this.schema, solution));
const query = this.driver.emitGoalsQuery(planQuery(this.schema, solution));
return { success: (await this.driver.run(query)).length > 0, sync: false };
}

Expand All @@ -70,7 +70,7 @@ export class SunBear<
goal: Goal,
): Query {
const solution = solveByPermission(this.schema, actor.constructor, actor, permission, goal, undefined);
return this.driver.emit(planQuery(this.schema, solution));
return this.driver.emitGoalsQuery(planQuery(this.schema, solution));
}

authorizedActorsQuery<Actor extends Node, Goal extends Node>(
Expand All @@ -79,6 +79,6 @@ export class SunBear<
goal: InstanceType<Goal>,
): Query {
const solution = solveByPermission(this.schema, actor, undefined, permission, goal.constructor, [goal]);
return this.driver.emit(planQuery(this.schema, solution));
return this.driver.emitActorsQuery(planQuery(this.schema, solution));
}
}
34 changes: 31 additions & 3 deletions src/drivers/PostgresDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ const simpleOwnershipPgDriver = new PostgresDriver(simpleOwnershipSchema, {} as
describe('PostgresDriver', () => {
it('compile: [simpleOwnership] optimized view', () => {
expect(
simpleOwnershipPgDriver.emit({
alternatives: [{ goalTable: Car, goalFilters: [], joins: [] }],
simpleOwnershipPgDriver.emitGoalsQuery({
alternatives: [{ goalTable: Car, actorTable: Person, goalFilters: [], joins: [] }],
}),
).toStrictEqual('(SELECT __goal.* FROM cars __goal)');
});

it('compile: [simpleOwnership] drive', () => {
expect(
simpleOwnershipPgDriver.emit({
simpleOwnershipPgDriver.emitGoalsQuery({
alternatives: [
{
goalTable: Car,
actorTable: Person,
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['a', 'b'] } }],
joins: [
{
Expand All @@ -40,4 +41,31 @@ describe('PostgresDriver', () => {
"(SELECT __goal.* FROM cars __goal INNER JOIN people __actor ON __actor.id = __goal.owner_id WHERE __goal.id IN ('a','b') AND __actor.id = '1')",
);
});

it('compile: [simpleOwnership] drivers', () => {
expect(
simpleOwnershipPgDriver.emitActorsQuery({
alternatives: [
{
goalTable: Car,
actorTable: Person,
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['a', 'b'] } }],
joins: [
{
kind: 'inner',
joinedTable: Person,
joinedTableName: '__actor',
joinedTableColumn: 'id',
existingTableName: '__goal',
existingTableColumn: 'owner_id',
filters: [{ column: 'id', condition: { kind: '=', value: '1' } }],
},
],
},
],
}),
).toEqual(
"(SELECT __actor.* FROM cars __goal INNER JOIN people __actor ON __actor.id = __goal.owner_id WHERE __goal.id IN ('a','b') AND __actor.id = '1')",
);
});
});
8 changes: 6 additions & 2 deletions src/drivers/PostgresDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export class PostgresDriver<Node, Relation, Role, Permission extends string> imp
protected readonly client: Client | Pool,
) {}

emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): string {
return emitSql(this.schema, query);
emitGoalsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): string {
return emitSql(this.schema, query, 'goal');
}

emitActorsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): string {
return emitSql(this.schema, query, 'actor');
}

async run<Goal extends Node>(query: string): Promise<Goal[]> {
Expand Down
29 changes: 27 additions & 2 deletions src/drivers/TypeOrmDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,36 @@ export class TypeOrmDriver<
}
}

emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): SelectQueryBuilder<Goal> {
emitGoalsQuery<Goal extends Node, Actor extends Node>(
query: PlannedQuery<Node, Goal, Actor>,
): SelectQueryBuilder<Goal> {
const goalPrimaryColumn = getNodeDefinition(this.schema, query.alternatives[0]!.goalTable).primaryColumn;
return this.dataSource
.createQueryBuilder(query.alternatives[0]!.goalTable, 'goal')
.where(`goal.${format.ident(goalPrimaryColumn)} IN (${emitSql(this.schema, query, goalPrimaryColumn)})`);
.where(
`goal.${format.ident(goalPrimaryColumn)} IN (${emitSql(
this.schema,
query,
'goal',
goalPrimaryColumn,
)})`,
);
}

emitActorsQuery<Goal extends Node, Actor extends Node>(
query: PlannedQuery<Node, Goal, Actor>,
): SelectQueryBuilder<Actor> {
const actorPrimaryColumn = getNodeDefinition(this.schema, query.alternatives[0]!.actorTable).primaryColumn;
return this.dataSource
.createQueryBuilder(query.alternatives[0]!.actorTable, 'actor')
.where(
`actor.${format.ident(actorPrimaryColumn)} IN (${emitSql(
this.schema,
query,
'actor',
actorPrimaryColumn,
)})`,
);
}

async run<Goal extends Node>(query: SelectQueryBuilder<Goal>): Promise<Goal[]> {
Expand Down
16 changes: 9 additions & 7 deletions src/emitSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import type { PlannedQuery, PlannedQueryJoin, PlannedQueryLinear } from './query

import { getNodeDefinition, RuleFilter, Schema } from '.';

export function emitSql<Node, Relation, Role, Permission extends string, Goal extends Node>(
export function emitSql<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
schema: Schema<Node, Relation, Role, Permission>,
query: PlannedQuery<Node, Goal>,
query: PlannedQuery<Node, Goal, Actor>,
selectTable: 'goal' | 'actor',
select: string | string[] = '*',
): string {
return query.alternatives.map((linear) => emitLinear(schema, linear, select)).join(' UNION ');
return query.alternatives.map((linear) => emitLinear(schema, linear, selectTable, select)).join(' UNION ');
}

function emitLinear<Node, Relation, Role, Permission extends string, Goal extends Node>(
function emitLinear<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
schema: Schema<Node, Relation, Role, Permission>,
linear: PlannedQueryLinear<Node, Goal>,
linear: PlannedQueryLinear<Node, Goal, Actor>,
selectTable: 'goal' | 'actor',
select: string | string[] = '*',
): string {
const sqlTableName = getNodeDefinition(schema, linear.goalTable).tableName;
Expand All @@ -24,11 +26,11 @@ function emitLinear<Node, Relation, Role, Permission extends string, Goal extend
const where = allFilters.length === 0 ? '' : ` WHERE ${allFilters.join(' AND ')}`;
const selectColumns =
select === '*'
? '__goal.*'
? `__${selectTable}.*`
: format
.ident(select)
.split(',')
.map((c) => `__goal.${c}`)
.map((c) => `__${selectTable}.${c}`)
.join(', ');
return `(SELECT ${selectColumns} FROM ${format.ident(sqlTableName)} __goal${joins
.map(({ join }) => ` ${join}`)
Expand Down
4 changes: 4 additions & 0 deletions src/queryPlan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ describe('queryPlan', () => {
alternatives: [
{
goalTable: Car,
actorTable: Person,
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['car1', 'car2'] } }],
joins: [],
},
{
goalTable: Car,
actorTable: Person,
goalFilters: [],
joins: [
{
Expand Down Expand Up @@ -101,6 +103,7 @@ describe('queryPlan', () => {
alternatives: [
{
goalTable: Project,
actorTable: User,
goalFilters: [],
joins: [
{
Expand Down Expand Up @@ -130,6 +133,7 @@ describe('queryPlan', () => {
},
{
goalTable: Project,
actorTable: User,
goalFilters: [],
joins: [
{
Expand Down
18 changes: 10 additions & 8 deletions src/queryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,32 @@ export interface PlannedQueryInnerJoin<Node> {

export type PlannedQueryJoin<Node> = PlannedQueryInnerJoin<Node> | PlannedQueryCrossJoin<Node>;

export interface PlannedQueryLinear<Node, Goal extends Node> {
export interface PlannedQueryLinear<Node, Goal extends Node, Actor extends Node> {
goalTable: Goal;
actorTable: Actor;
goalFilters: readonly RuleFilter[];
joins: readonly PlannedQueryJoin<Node>[];
}

export interface PlannedQueryUnion<Node, Goal extends Node> {
alternatives: readonly PlannedQueryLinear<Node, Goal>[];
export interface PlannedQueryUnion<Node, Goal extends Node, Actor extends Node> {
alternatives: readonly PlannedQueryLinear<Node, Goal, Actor>[];
}

export type PlannedQuery<Node, Goal extends Node> = PlannedQueryUnion<Node, Goal>;
export type PlannedQuery<Node, Goal extends Node, Actor extends Node> = PlannedQueryUnion<Node, Goal, Actor>;

export function planQuery<Node, Relation, Role, Permission extends string, Goal extends Node>(
export function planQuery<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
schema: Schema<Node, Relation, Role, Permission>,
solutions: Solution<Node, Relation, Role, Goal>,
): PlannedQuery<Node, Goal> {
): PlannedQuery<Node, Goal, Actor> {
return {
alternatives: solutions.map((chain) => planRuleChain(schema, chain)),
};
}

export function planRuleChain<Node, Relation, Role, Permission extends string, Goal extends Node>(
export function planRuleChain<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
schema: Schema<Node, Relation, Role, Permission>,
chain: SolutionChain<Node, Relation, Role>,
): PlannedQueryLinear<Node, Goal> {
): PlannedQueryLinear<Node, Goal, Actor> {
const goalRule = chain.extensions.length === 0 ? chain.direct : chain.extensions[chain.extensions.length - 1]!;
const goalNodeDefinition = getNodeDefinition(schema, goalRule.node);
const goalFilters = [...(goalNodeDefinition.defaultFilters ?? []), ...(goalRule.filters ?? [])];
Expand Down Expand Up @@ -136,6 +137,7 @@ export function planRuleChain<Node, Relation, Role, Permission extends string, G

return {
goalTable: goalRule.node as Goal,
actorTable: rule.actorNode as Actor,
goalFilters,
joins,
};
Expand Down

0 comments on commit ccd63cd

Please sign in to comment.