Skip to content

Commit b43b575

Browse files
committed
refactor(microorm-database): add ACL test entities, migrations, and seeders for permissions testing
1 parent f2dd856 commit b43b575

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3667
-13
lines changed

libs/microorm-database/src/lib/config-cli.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Options } from '@mikro-orm/core';
22
import { TSMigrationGenerator } from '@mikro-orm/migrations';
33
import { join } from 'node:path';
44
import { pgConfig } from './config-pg';
5+
import { PGlite } from '@electric-sql/pglite';
56

67
const config: Options = {
78
...pgConfig,
@@ -16,6 +17,21 @@ const config: Options = {
1617
emit: 'ts',
1718
generator: TSMigrationGenerator,
1819
},
20+
seeder: {
21+
path: join(__dirname, './seeders'),
22+
}
1923
};
2024

21-
export default config;
25+
export default Promise.resolve(config).then(async (configR) => {
26+
27+
// @ts-ignore
28+
const {driverOptions: {connection: {pglite: pgLiteCall}}} = configR;
29+
const pgLite: PGlite = pgLiteCall();
30+
await pgLite.waitReady
31+
32+
// not parser array
33+
// pgLite.parsers[1003] = (...arg: any[]) => arg[0]
34+
35+
36+
return config
37+
});

libs/microorm-database/src/lib/config-pg/pg-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { PGlite } from '@electric-sql/pglite';
99
import { PGliteDriver, PGliteConnectionConfig } from "mikro-orm-pglite";
1010
import * as allEntities from '../entities';
1111

12-
const pgDir = process.env['TEST'] ? './tmp/pg-test/mikroorm' : './tmp/pg/mikroorm'
12+
const pgDir = process.env['TEST'] ? './tmp/pg-test/microorm' : './tmp/pg/microorm'
1313

1414
mkdirSync(pgDir, { recursive: true });
1515

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { pgConfig } from './config-pg';
2+
import { ClsServiceManager } from 'nestjs-cls';
3+
import { EntityManager } from '@mikro-orm/core';
4+
import { DatabaseLoggerService } from './database-logger.service';
5+
import type { LoggerOptions } from '@mikro-orm/core';
6+
export const CONTEXT_STORE_NAME = Symbol('mikroorm-database');
27

38
export const config = {
49
...pgConfig,
510
contextName: 'default',
11+
loggerFactory: (options: LoggerOptions) => new DatabaseLoggerService(options),
12+
context: () => ClsServiceManager.getClsService().get<EntityManager>(CONTEXT_STORE_NAME),
613
// @ts-ignore
714
registerRequestContext: false
815
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Logger as NestJsLogger } from '@nestjs/common';
2+
3+
import type { LoggerOptions } from '@mikro-orm/core';
4+
import type { Highlighter } from '@mikro-orm/core';
5+
import type { LogContext, LoggerNamespace } from '@mikro-orm/postgresql';
6+
import { DefaultLogger } from '@mikro-orm/postgresql';
7+
8+
export type AnyString = string & {};
9+
10+
export class DatabaseLoggerService extends DefaultLogger {
11+
private static logger = new NestJsLogger('MikroOrmDatabaseModule');
12+
private useHighlighter: Highlighter | undefined = undefined;
13+
private hasReplicas: boolean | undefined = undefined;
14+
15+
constructor(options: LoggerOptions) {
16+
super(options);
17+
this.useHighlighter = options.highlighter;
18+
this.hasReplicas = options.usesReplicas;
19+
}
20+
21+
override log(
22+
namespace: LoggerNamespace | AnyString,
23+
message: string,
24+
context?: LogContext,
25+
): void {
26+
if (namespace === 'info' && !this.useHighlighter) {
27+
message = message.replace(
28+
// eslint-disable-next-line no-control-regex
29+
/\x1b\[[0-9;]*m/g,
30+
'',
31+
);
32+
}
33+
34+
message = message.replace(/\n/g, '').replace(/ +/g, ' ').trim();
35+
36+
if (context && 'contextName' in context) {
37+
namespace = `${namespace}:${context['contextName']}`;
38+
}
39+
const messageResult = `(${namespace}) ${message}`;
40+
let typeLog: 'debug' | 'error' | 'warn' = 'debug';
41+
switch (context?.level) {
42+
case 'error':
43+
typeLog = 'debug';
44+
break;
45+
case 'warning':
46+
typeLog = 'warn';
47+
break;
48+
}
49+
50+
DatabaseLoggerService.logger[typeLog](messageResult);
51+
}
52+
53+
override logQuery(context: { query: string } & LogContext): void {
54+
if (this.useHighlighter) {
55+
return super.logQuery(context);
56+
}
57+
58+
let msg = context.query;
59+
60+
if (context.took != null) {
61+
const meta = [`took ${context.took} ms`];
62+
63+
if (context.results != null) {
64+
meta.push(
65+
`${context.results} result${
66+
context.results === 0 || context.results > 1 ? 's' : ''
67+
}`,
68+
);
69+
}
70+
71+
if (context.affected != null) {
72+
meta.push(
73+
`${context.affected} row${
74+
context.affected === 0 || context.affected > 1 ? 's' : ''
75+
} affected`,
76+
);
77+
}
78+
79+
msg += ` [${meta.join(', ')}]`;
80+
}
81+
82+
if (this.hasReplicas && context.connection) {
83+
msg += ` (via ${context.connection.type} connection '${context.connection.name}')`;
84+
}
85+
86+
return this.log('query', msg, context);
87+
}
88+
89+
// setDebugMode(debugMode: boolean | LoggerNamespace[]): void {
90+
// console.log('setDebugMode', debugMode);
91+
// // throw new Error('Method not implemented.');
92+
// }
93+
// isEnabled(namespace: LoggerNamespace, context?: LogContext): boolean {
94+
// console.log('isEnabled', namespace, context);
95+
// // throw new Error('Method not implemented.');
96+
// return true;
97+
// }
98+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import {
2+
Entity,
3+
PrimaryKey,
4+
Property,
5+
ManyToOne,
6+
Enum,
7+
ArrayType,
8+
} from '@mikro-orm/core';
9+
import { UsersAcl, IUsersAcl } from './user.entity';
10+
11+
export enum ArticleStatus {
12+
DRAFT = 'draft',
13+
REVIEW = 'review',
14+
PUBLISHED = 'published',
15+
}
16+
17+
export enum ArticleVisibility {
18+
PUBLIC = 'public',
19+
PRIVATE = 'private',
20+
UNLISTED = 'unlisted',
21+
}
22+
23+
export interface ArticleMetadata {
24+
readTime: number;
25+
featured: boolean;
26+
premium: boolean;
27+
}
28+
29+
export type IArticleAcl = ArticleAcl;
30+
31+
/**
32+
* Article entity for ACL testing - Complex scenarios
33+
*
34+
* ACL Test Cases:
35+
* - Multiple owners (authorId OR coAuthorIds.includes(userId))
36+
* - Array conditions (checking if userId in coAuthorIds array)
37+
* - Nested object conditions (metadata.premium)
38+
* - Time-based access (expiresAt > now)
39+
* - Complex workflows (draft -> review -> published)
40+
* - Editor role (separate from author)
41+
* - Template: ${@input.coAuthorIds}, ${metadata.premium}, ${currentTime}
42+
*/
43+
@Entity({
44+
tableName: 'acl_articles',
45+
})
46+
export class ArticleAcl {
47+
@PrimaryKey({
48+
autoincrement: true,
49+
})
50+
public id!: number;
51+
52+
@Property({
53+
type: 'varchar',
54+
length: 255,
55+
nullable: false,
56+
})
57+
public title!: string;
58+
59+
@Property({
60+
type: 'text',
61+
nullable: false,
62+
})
63+
public content!: string;
64+
65+
/**
66+
* Primary author (ownership)
67+
*/
68+
@ManyToOne(() => UsersAcl, {
69+
nullable: false,
70+
fieldName: 'author_id',
71+
})
72+
public author!: IUsersAcl;
73+
74+
/**
75+
* Co-authors array for multiple ownership testing
76+
* ACL: Check if currentUserId in this array
77+
*/
78+
@Property({
79+
name: 'co_author_ids',
80+
type: ArrayType<number>,
81+
columnType: 'integer[]',
82+
default: [],
83+
})
84+
public coAuthorIds!: number[];
85+
86+
/**
87+
* Editor (different from author/co-authors)
88+
* Can edit but not delete
89+
*/
90+
@ManyToOne(() => UsersAcl, {
91+
nullable: true,
92+
fieldName: 'editor_id',
93+
})
94+
public editor!: IUsersAcl | null;
95+
96+
/**
97+
* Workflow status
98+
*/
99+
@Enum(() => ArticleStatus)
100+
@Property({
101+
type: 'varchar',
102+
length: 20,
103+
default: ArticleStatus.DRAFT,
104+
})
105+
public status!: ArticleStatus;
106+
107+
/**
108+
* Visibility control
109+
*/
110+
@Enum(() => ArticleVisibility)
111+
@Property({
112+
type: 'varchar',
113+
length: 20,
114+
default: ArticleVisibility.PUBLIC,
115+
})
116+
public visibility!: ArticleVisibility;
117+
118+
/**
119+
* Metadata as JSON object
120+
* ACL: Check nested properties like metadata.premium
121+
*/
122+
@Property({
123+
type: 'json',
124+
nullable: false,
125+
default: '{"readTime": 0, "featured": false, "premium": false}',
126+
})
127+
public metadata!: ArticleMetadata;
128+
129+
/**
130+
* Publish date for time-based access
131+
*/
132+
@Property({
133+
length: 0,
134+
name: 'published_at',
135+
nullable: true,
136+
columnType: 'timestamp(0) without time zone',
137+
type: 'timestamp',
138+
})
139+
public publishedAt!: Date | null;
140+
141+
/**
142+
* Expiration date for temporary access
143+
* ACL: Check if current time < expiresAt
144+
*/
145+
@Property({
146+
length: 0,
147+
name: 'expires_at',
148+
nullable: true,
149+
columnType: 'timestamp(0) without time zone',
150+
type: 'timestamp',
151+
})
152+
public expiresAt!: Date | null;
153+
154+
@Property({
155+
length: 0,
156+
name: 'created_at',
157+
nullable: false,
158+
defaultRaw: 'CURRENT_TIMESTAMP(0)',
159+
columnType: 'timestamp(0) without time zone',
160+
type: 'timestamp',
161+
})
162+
createdAt: Date = new Date();
163+
164+
@Property({
165+
length: 0,
166+
onUpdate: () => new Date(),
167+
name: 'updated_at',
168+
nullable: false,
169+
columnType: 'timestamp(0) without time zone',
170+
defaultRaw: 'CURRENT_TIMESTAMP(0)',
171+
})
172+
updatedAt: Date = new Date();
173+
}

0 commit comments

Comments
 (0)