Skip to content

Commit c322379

Browse files
committed
feat(json-api-nestjs-typeorm): add ACL rule support for query builders and enhance getOne and getAll methods with additional query params and transform toggles
1 parent d5c1ea7 commit c322379

File tree

7 files changed

+692
-28
lines changed

7 files changed

+692
-28
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { DataSource, Repository, Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
2+
import { PGliteDriver } from 'typeorm-pglite';
3+
import { applyAclRulesToQueryBuilder } from './acl-rules-to-typeorm';
4+
import { TypeormUtilsService } from '../service';
5+
6+
@Entity()
7+
class User {
8+
@PrimaryGeneratedColumn()
9+
id!: number;
10+
11+
@Column()
12+
name!: string;
13+
}
14+
15+
@Entity()
16+
class TestEntity {
17+
@PrimaryGeneratedColumn()
18+
id!: number;
19+
20+
@Column()
21+
authorId!: number;
22+
23+
@Column()
24+
status!: string;
25+
26+
@Column({ default: false })
27+
isPublic!: boolean;
28+
29+
@Column()
30+
lastName!: string;
31+
32+
@ManyToOne(() => User)
33+
@JoinColumn()
34+
user?: User;
35+
}
36+
37+
describe('applyAclRulesToQueryBuilder', () => {
38+
let dataSource: DataSource;
39+
let repository: Repository<TestEntity>;
40+
let typeormUtils: TypeormUtilsService<TestEntity, 'id'>;
41+
42+
beforeAll(async () => {
43+
dataSource = new DataSource({
44+
type: 'postgres',
45+
driver: new PGliteDriver({}).driver,
46+
entities: [TestEntity, User],
47+
synchronize: true,
48+
});
49+
50+
await dataSource.initialize();
51+
repository = dataSource.getRepository(TestEntity);
52+
typeormUtils = new (TypeormUtilsService as any)(repository);
53+
});
54+
55+
afterAll(async () => {
56+
if (dataSource?.isInitialized) {
57+
await dataSource.destroy();
58+
}
59+
});
60+
61+
it('should handle $eq operator', () => {
62+
const rulesForQuery = { authorId: { $eq: 123 } };
63+
64+
const queryBuilder = repository.createQueryBuilder('TestEntity');
65+
const brackets = applyAclRulesToQueryBuilder(rulesForQuery, typeormUtils);
66+
queryBuilder.where(brackets);
67+
68+
const sql = queryBuilder.getQuery();
69+
const params = queryBuilder.getParameters();
70+
71+
expect(sql).toContain('"TestEntity"."authorId" = :aclParam_1');
72+
expect(params['aclParam_1']).toBe(123);
73+
});
74+
75+
it('should handle primitive value (direct equality)', () => {
76+
const rulesForQuery = { status: 'published' };
77+
78+
const queryBuilder = repository.createQueryBuilder('TestEntity');
79+
const brackets = applyAclRulesToQueryBuilder(rulesForQuery, typeormUtils);
80+
queryBuilder.where(brackets);
81+
82+
const sql = queryBuilder.getQuery();
83+
const params = queryBuilder.getParameters();
84+
85+
expect(sql).toContain('"TestEntity"."status" = :aclParam_1');
86+
expect(params['aclParam_1']).toBe('published');
87+
});
88+
89+
it('should handle $in operator', () => {
90+
const rulesForQuery = { status: { $in: ['published', 'archived'] } };
91+
92+
const queryBuilder = repository.createQueryBuilder('TestEntity');
93+
const brackets = applyAclRulesToQueryBuilder(rulesForQuery, typeormUtils);
94+
queryBuilder.where(brackets);
95+
96+
const sql = queryBuilder.getQuery();
97+
const params = queryBuilder.getParameters();
98+
99+
expect(sql).toContain('"TestEntity"."status" IN (:...aclParam_1)');
100+
expect(params['aclParam_1']).toEqual(['published', 'archived']);
101+
});
102+
103+
it('should handle $or operator', () => {
104+
const rulesForQuery = { $or: [{ authorId: 123 }, { status: 'published' }] };
105+
106+
const queryBuilder = repository.createQueryBuilder('TestEntity');
107+
const brackets = applyAclRulesToQueryBuilder(rulesForQuery, typeormUtils);
108+
queryBuilder.where(brackets);
109+
110+
const sql = queryBuilder.getQuery();
111+
const params = queryBuilder.getParameters();
112+
113+
// Should contain both conditions with OR logic
114+
expect(sql).toContain('"TestEntity"."authorId" = :aclParam_1');
115+
expect(sql).toContain('"TestEntity"."status" = :aclParam_2');
116+
expect(sql).toContain('OR'); // Should have OR operator
117+
expect(params['aclParam_1']).toBe(123);
118+
expect(params['aclParam_2']).toBe('published');
119+
});
120+
121+
it('should handle complex query with $or, $and, $not and relations', () => {
122+
const rulesForQuery = {
123+
$or: [
124+
{ isPublic: { $eq: true } },
125+
{ user: { id: { $eq: 3 } } },
126+
{ lastName: 'test' },
127+
],
128+
$and: [
129+
{ $not: { lastName: 'test2' } },
130+
],
131+
};
132+
133+
const queryBuilder = repository.createQueryBuilder('TestEntity');
134+
const brackets = applyAclRulesToQueryBuilder(rulesForQuery, typeormUtils);
135+
queryBuilder.where(brackets);
136+
137+
const sql = queryBuilder.getQuery();
138+
const params = queryBuilder.getParameters();
139+
140+
// Check $or conditions
141+
expect(sql).toContain('OR');
142+
expect(sql).toContain('"TestEntity"."isPublic" = :aclParam_1');
143+
expect(params['aclParam_1']).toBe(true);
144+
145+
// Check relation field (user.id)
146+
expect(sql).toContain('user'); // Should have user relation alias
147+
expect(sql).toContain('id'); // Should have id field
148+
expect(params['aclParam_2']).toBe(3);
149+
150+
// Check primitive lastName
151+
expect(sql).toContain('"TestEntity"."lastName" = :aclParam_3');
152+
expect(params['aclParam_3']).toBe('test');
153+
154+
// Check $and and $not
155+
expect(sql).toContain('AND');
156+
// $not is not fully supported, should have warning logged
157+
});
158+
});

0 commit comments

Comments
 (0)