Skip to content

Commit ccd63cd

Browse files
committed
fix actors queries
1 parent a215464 commit ccd63cd

10 files changed

Lines changed: 96 additions & 30 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sunbear",
3-
"version": "0.0.6",
3+
"version": "0.0.7",
44
"description": "Authorization library for SQL",
55
"main": "dist/src/index.js",
66
"types": "dist/src/index.d.ts",

src/Driver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { PlannedQuery } from './queryPlan';
22

33
export interface Driver<Query, Node> {
4-
emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): Query;
4+
emitGoalsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): Query;
5+
emitActorsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): Query;
56
run<Goal extends Node>(query: Query): Promise<Goal[]>;
67
}

src/SunBear.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class SunBear<
4444
if (solution === true) {
4545
return true;
4646
}
47-
const query = this.driver.emit(planQuery(this.schema, solution));
47+
const query = this.driver.emitGoalsQuery(planQuery(this.schema, solution));
4848
return (await this.driver.run(query)).length > 0;
4949
}
5050

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

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

7676
authorizedActorsQuery<Actor extends Node, Goal extends Node>(
@@ -79,6 +79,6 @@ export class SunBear<
7979
goal: InstanceType<Goal>,
8080
): Query {
8181
const solution = solveByPermission(this.schema, actor, undefined, permission, goal.constructor, [goal]);
82-
return this.driver.emit(planQuery(this.schema, solution));
82+
return this.driver.emitActorsQuery(planQuery(this.schema, solution));
8383
}
8484
}

src/drivers/PostgresDriver.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ const simpleOwnershipPgDriver = new PostgresDriver(simpleOwnershipSchema, {} as
99
describe('PostgresDriver', () => {
1010
it('compile: [simpleOwnership] optimized view', () => {
1111
expect(
12-
simpleOwnershipPgDriver.emit({
13-
alternatives: [{ goalTable: Car, goalFilters: [], joins: [] }],
12+
simpleOwnershipPgDriver.emitGoalsQuery({
13+
alternatives: [{ goalTable: Car, actorTable: Person, goalFilters: [], joins: [] }],
1414
}),
1515
).toStrictEqual('(SELECT __goal.* FROM cars __goal)');
1616
});
1717

1818
it('compile: [simpleOwnership] drive', () => {
1919
expect(
20-
simpleOwnershipPgDriver.emit({
20+
simpleOwnershipPgDriver.emitGoalsQuery({
2121
alternatives: [
2222
{
2323
goalTable: Car,
24+
actorTable: Person,
2425
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['a', 'b'] } }],
2526
joins: [
2627
{
@@ -40,4 +41,31 @@ describe('PostgresDriver', () => {
4041
"(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')",
4142
);
4243
});
44+
45+
it('compile: [simpleOwnership] drivers', () => {
46+
expect(
47+
simpleOwnershipPgDriver.emitActorsQuery({
48+
alternatives: [
49+
{
50+
goalTable: Car,
51+
actorTable: Person,
52+
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['a', 'b'] } }],
53+
joins: [
54+
{
55+
kind: 'inner',
56+
joinedTable: Person,
57+
joinedTableName: '__actor',
58+
joinedTableColumn: 'id',
59+
existingTableName: '__goal',
60+
existingTableColumn: 'owner_id',
61+
filters: [{ column: 'id', condition: { kind: '=', value: '1' } }],
62+
},
63+
],
64+
},
65+
],
66+
}),
67+
).toEqual(
68+
"(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')",
69+
);
70+
});
4371
});

src/drivers/PostgresDriver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ export class PostgresDriver<Node, Relation, Role, Permission extends string> imp
1111
protected readonly client: Client | Pool,
1212
) {}
1313

14-
emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): string {
15-
return emitSql(this.schema, query);
14+
emitGoalsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): string {
15+
return emitSql(this.schema, query, 'goal');
16+
}
17+
18+
emitActorsQuery<Goal extends Node, Actor extends Node>(query: PlannedQuery<Node, Goal, Actor>): string {
19+
return emitSql(this.schema, query, 'actor');
1620
}
1721

1822
async run<Goal extends Node>(query: string): Promise<Goal[]> {

src/drivers/TypeOrmDriver.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,36 @@ export class TypeOrmDriver<
2525
}
2626
}
2727

28-
emit<Goal extends Node>(query: PlannedQuery<Node, Goal>): SelectQueryBuilder<Goal> {
28+
emitGoalsQuery<Goal extends Node, Actor extends Node>(
29+
query: PlannedQuery<Node, Goal, Actor>,
30+
): SelectQueryBuilder<Goal> {
2931
const goalPrimaryColumn = getNodeDefinition(this.schema, query.alternatives[0]!.goalTable).primaryColumn;
3032
return this.dataSource
3133
.createQueryBuilder(query.alternatives[0]!.goalTable, 'goal')
32-
.where(`goal.${format.ident(goalPrimaryColumn)} IN (${emitSql(this.schema, query, goalPrimaryColumn)})`);
34+
.where(
35+
`goal.${format.ident(goalPrimaryColumn)} IN (${emitSql(
36+
this.schema,
37+
query,
38+
'goal',
39+
goalPrimaryColumn,
40+
)})`,
41+
);
42+
}
43+
44+
emitActorsQuery<Goal extends Node, Actor extends Node>(
45+
query: PlannedQuery<Node, Goal, Actor>,
46+
): SelectQueryBuilder<Actor> {
47+
const actorPrimaryColumn = getNodeDefinition(this.schema, query.alternatives[0]!.actorTable).primaryColumn;
48+
return this.dataSource
49+
.createQueryBuilder(query.alternatives[0]!.actorTable, 'actor')
50+
.where(
51+
`actor.${format.ident(actorPrimaryColumn)} IN (${emitSql(
52+
this.schema,
53+
query,
54+
'actor',
55+
actorPrimaryColumn,
56+
)})`,
57+
);
3358
}
3459

3560
async run<Goal extends Node>(query: SelectQueryBuilder<Goal>): Promise<Goal[]> {

src/emitSql.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import type { PlannedQuery, PlannedQueryJoin, PlannedQueryLinear } from './query
44

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

7-
export function emitSql<Node, Relation, Role, Permission extends string, Goal extends Node>(
7+
export function emitSql<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
88
schema: Schema<Node, Relation, Role, Permission>,
9-
query: PlannedQuery<Node, Goal>,
9+
query: PlannedQuery<Node, Goal, Actor>,
10+
selectTable: 'goal' | 'actor',
1011
select: string | string[] = '*',
1112
): string {
12-
return query.alternatives.map((linear) => emitLinear(schema, linear, select)).join(' UNION ');
13+
return query.alternatives.map((linear) => emitLinear(schema, linear, selectTable, select)).join(' UNION ');
1314
}
1415

15-
function emitLinear<Node, Relation, Role, Permission extends string, Goal extends Node>(
16+
function emitLinear<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
1617
schema: Schema<Node, Relation, Role, Permission>,
17-
linear: PlannedQueryLinear<Node, Goal>,
18+
linear: PlannedQueryLinear<Node, Goal, Actor>,
19+
selectTable: 'goal' | 'actor',
1820
select: string | string[] = '*',
1921
): string {
2022
const sqlTableName = getNodeDefinition(schema, linear.goalTable).tableName;
@@ -24,11 +26,11 @@ function emitLinear<Node, Relation, Role, Permission extends string, Goal extend
2426
const where = allFilters.length === 0 ? '' : ` WHERE ${allFilters.join(' AND ')}`;
2527
const selectColumns =
2628
select === '*'
27-
? '__goal.*'
29+
? `__${selectTable}.*`
2830
: format
2931
.ident(select)
3032
.split(',')
31-
.map((c) => `__goal.${c}`)
33+
.map((c) => `__${selectTable}.${c}`)
3234
.join(', ');
3335
return `(SELECT ${selectColumns} FROM ${format.ident(sqlTableName)} __goal${joins
3436
.map(({ join }) => ` ${join}`)

src/queryPlan.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ describe('queryPlan', () => {
3535
alternatives: [
3636
{
3737
goalTable: Car,
38+
actorTable: Person,
3839
goalFilters: [{ column: 'id', condition: { kind: 'IN', values: ['car1', 'car2'] } }],
3940
joins: [],
4041
},
4142
{
4243
goalTable: Car,
44+
actorTable: Person,
4345
goalFilters: [],
4446
joins: [
4547
{
@@ -101,6 +103,7 @@ describe('queryPlan', () => {
101103
alternatives: [
102104
{
103105
goalTable: Project,
106+
actorTable: User,
104107
goalFilters: [],
105108
joins: [
106109
{
@@ -130,6 +133,7 @@ describe('queryPlan', () => {
130133
},
131134
{
132135
goalTable: Project,
136+
actorTable: User,
133137
goalFilters: [],
134138
joins: [
135139
{

src/queryPlan.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,32 @@ export interface PlannedQueryInnerJoin<Node> {
2222

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

25-
export interface PlannedQueryLinear<Node, Goal extends Node> {
25+
export interface PlannedQueryLinear<Node, Goal extends Node, Actor extends Node> {
2626
goalTable: Goal;
27+
actorTable: Actor;
2728
goalFilters: readonly RuleFilter[];
2829
joins: readonly PlannedQueryJoin<Node>[];
2930
}
3031

31-
export interface PlannedQueryUnion<Node, Goal extends Node> {
32-
alternatives: readonly PlannedQueryLinear<Node, Goal>[];
32+
export interface PlannedQueryUnion<Node, Goal extends Node, Actor extends Node> {
33+
alternatives: readonly PlannedQueryLinear<Node, Goal, Actor>[];
3334
}
3435

35-
export type PlannedQuery<Node, Goal extends Node> = PlannedQueryUnion<Node, Goal>;
36+
export type PlannedQuery<Node, Goal extends Node, Actor extends Node> = PlannedQueryUnion<Node, Goal, Actor>;
3637

37-
export function planQuery<Node, Relation, Role, Permission extends string, Goal extends Node>(
38+
export function planQuery<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
3839
schema: Schema<Node, Relation, Role, Permission>,
3940
solutions: Solution<Node, Relation, Role, Goal>,
40-
): PlannedQuery<Node, Goal> {
41+
): PlannedQuery<Node, Goal, Actor> {
4142
return {
4243
alternatives: solutions.map((chain) => planRuleChain(schema, chain)),
4344
};
4445
}
4546

46-
export function planRuleChain<Node, Relation, Role, Permission extends string, Goal extends Node>(
47+
export function planRuleChain<Node, Relation, Role, Permission extends string, Goal extends Node, Actor extends Node>(
4748
schema: Schema<Node, Relation, Role, Permission>,
4849
chain: SolutionChain<Node, Relation, Role>,
49-
): PlannedQueryLinear<Node, Goal> {
50+
): PlannedQueryLinear<Node, Goal, Actor> {
5051
const goalRule = chain.extensions.length === 0 ? chain.direct : chain.extensions[chain.extensions.length - 1]!;
5152
const goalNodeDefinition = getNodeDefinition(schema, goalRule.node);
5253
const goalFilters = [...(goalNodeDefinition.defaultFilters ?? []), ...(goalRule.filters ?? [])];
@@ -136,6 +137,7 @@ export function planRuleChain<Node, Relation, Role, Permission extends string, G
136137

137138
return {
138139
goalTable: goalRule.node as Goal,
140+
actorTable: rule.actorNode as Actor,
139141
goalFilters,
140142
joins,
141143
};

0 commit comments

Comments
 (0)