Skip to content

Commit 206a514

Browse files
authored
Merge pull request #6377 from bcgov/refactor/6115
feat(6115): improve event service design pattern
2 parents 12f952e + 12fcd25 commit 206a514

File tree

6 files changed

+108
-97
lines changed

6 files changed

+108
-97
lines changed

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ install:
2525
pnpm --dir sandbox/keycloak-provision install
2626
pnpm --dir sandbox/m365mock install
2727
pnpm --dir sandbox/nats-provision install
28+
install: canvas-install
29+
30+
.PHONY: canvas-install
31+
canvas-install:
32+
cd app/node_modules/.pnpm/canvas@3.2.0/node_modules/canvas &&\
33+
pnpm add -D node-gyp &&\
34+
pnpm exec node-gyp rebuild
2835

2936
.PHONY: asdf-install
3037
asdf-install:
31-
cat .tool-versions | cut -f 1 -d ' ' | xargs -n 1 asdf plugin-add || true
38+
cat .tool-versions | cut -f 1 -d ' ' | xargs -n 1 asdf plugin add || true
3239
asdf plugin update --all
3340
asdf install || true
3441
asdf reshim

app/app/api/keycloak/api-accounts/team/_operations/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default async function getOp({
6868

6969
if (client) {
7070
await createEvent(EventType.CREATE_TEAM_API_TOKEN, session.user.id, {
71-
clientUid: client.id,
71+
clientUid: client.id!,
7272
});
7373
}
7474

app/app/api/keycloak/api-accounts/team/_operations/update.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default async function getOp({
7878
});
7979

8080
if (client) {
81-
await createEvent(EventType.UPDATE_TEAM_API_TOKEN, session.user.id, { clientUid: client.id, roles });
81+
await createEvent(EventType.UPDATE_TEAM_API_TOKEN, session.user.id, { clientUid: client.id!, roles });
8282
}
8383

8484
return OkResponse({ client, user });

app/services/db/event.ts

Lines changed: 90 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,60 @@ import { objectId } from '@/validation-schemas';
99
import { EventSearchBody } from '@/validation-schemas/event';
1010
import { organizationBodySchema } from '@/validation-schemas/organization';
1111

12+
const requestIdSchema = z.object({
13+
requestId: z.string().length(24),
14+
});
15+
16+
const licencePlateSchema = z.object({
17+
licencePlate: z.string().min(6),
18+
});
19+
1220
const validationSchemas = {
13-
[EventType.CREATE_PRIVATE_CLOUD_PRODUCT]: z.object({
14-
requestId: z.string().length(24),
15-
}),
16-
[EventType.UPDATE_PRIVATE_CLOUD_PRODUCT]: z.object({
17-
requestId: z.string().length(24),
18-
}),
19-
[EventType.DELETE_PRIVATE_CLOUD_PRODUCT]: z.object({
20-
requestId: z.string().length(24),
21-
}),
22-
[EventType.REVIEW_PRIVATE_CLOUD_REQUEST]: z.object({
23-
requestId: z.string().length(24),
24-
}),
25-
[EventType.RESEND_PRIVATE_CLOUD_REQUEST]: z.object({
26-
requestId: z.string().length(24),
27-
}),
28-
[EventType.REPROVISION_PRIVATE_CLOUD_PRODUCT]: z.object({
29-
licencePlate: z.string().min(6),
21+
[EventType.CREATE_TEAM_API_TOKEN]: z.object({
22+
clientUid: z.string(),
3023
}),
31-
[EventType.CREATE_PUBLIC_CLOUD_PRODUCT]: z.object({
32-
requestId: z.string().length(24),
24+
[EventType.UPDATE_TEAM_API_TOKEN]: z.object({
25+
clientUid: z.string(),
26+
users: z
27+
.array(
28+
z.object({
29+
email: z.string().email(),
30+
type: z.enum(['add', 'remove']),
31+
}),
32+
)
33+
.optional(),
34+
roles: z.array(z.string()).optional(),
35+
result: z
36+
.array(
37+
z.object({
38+
success: z.boolean(),
39+
email: z.email(),
40+
error: z.string().optional(),
41+
}),
42+
)
43+
.optional(),
3344
}),
34-
[EventType.UPDATE_PUBLIC_CLOUD_PRODUCT]: z.object({
35-
requestId: z.string().length(24),
36-
}),
37-
[EventType.DELETE_PUBLIC_CLOUD_PRODUCT]: z.object({
38-
requestId: z.string().length(24),
39-
}),
40-
[EventType.REVIEW_PUBLIC_CLOUD_REQUEST]: z.object({
41-
requestId: z.string().length(24),
42-
}),
43-
[EventType.RESEND_PUBLIC_CLOUD_REQUEST]: z.object({
44-
requestId: z.string().length(24),
45+
[EventType.DELETE_TEAM_API_TOKEN]: z.object({
46+
clientUid: z.string(),
4547
}),
48+
[EventType.CREATE_API_TOKEN]: z.undefined(),
49+
[EventType.DELETE_API_TOKEN]: z.undefined(),
50+
51+
[EventType.CREATE_PRIVATE_CLOUD_PRODUCT]: requestIdSchema,
52+
[EventType.UPDATE_PRIVATE_CLOUD_PRODUCT]: requestIdSchema,
53+
[EventType.DELETE_PRIVATE_CLOUD_PRODUCT]: requestIdSchema,
54+
[EventType.REVIEW_PRIVATE_CLOUD_REQUEST]: requestIdSchema,
55+
[EventType.RESEND_PRIVATE_CLOUD_REQUEST]: requestIdSchema,
56+
[EventType.CANCEL_PRIVATE_CLOUD_REQUEST]: requestIdSchema,
57+
[EventType.REPROVISION_PRIVATE_CLOUD_PRODUCT]: licencePlateSchema,
58+
59+
[EventType.CREATE_PUBLIC_CLOUD_PRODUCT]: requestIdSchema,
60+
[EventType.UPDATE_PUBLIC_CLOUD_PRODUCT]: requestIdSchema,
61+
[EventType.DELETE_PUBLIC_CLOUD_PRODUCT]: requestIdSchema,
62+
[EventType.REVIEW_PUBLIC_CLOUD_REQUEST]: requestIdSchema,
63+
[EventType.RESEND_PUBLIC_CLOUD_REQUEST]: requestIdSchema,
64+
[EventType.CANCEL_PUBLIC_CLOUD_REQUEST]: requestIdSchema,
65+
4666
[EventType.CREATE_ORGANIZATION]: z.object({
4767
id: objectId,
4868
data: organizationBodySchema,
@@ -54,10 +74,50 @@ const validationSchemas = {
5474
}),
5575
[EventType.DELETE_ORGANIZATION]: z.object({
5676
id: objectId,
77+
to: objectId,
5778
data: organizationBodySchema,
5879
}),
80+
[EventType.LOGIN]: z.undefined(),
81+
[EventType.LOGOUT]: z.undefined(),
82+
[EventType.EXPORT_PRIVATE_CLOUD_PRODUCT]: z.any(),
83+
[EventType.EXPORT_PUBLIC_CLOUD_PRODUCT]: z.any(),
84+
};
85+
86+
type EventDataMap = {
87+
[K in keyof typeof validationSchemas]: z.infer<(typeof validationSchemas)[K]>;
5988
};
6089

90+
export async function createEvent<T extends keyof typeof validationSchemas>(
91+
type: T,
92+
userId = '',
93+
data?: EventDataMap[T],
94+
) {
95+
try {
96+
const validationSchema = validationSchemas[type];
97+
98+
// Validate only if schema is not z.any()
99+
if (validationSchema && validationSchema !== z.any()) {
100+
const parsed = validationSchema.safeParse(data);
101+
if (!parsed.success) {
102+
throw new Error(`Invalid data for event type ${type}: ${JSON.stringify(parsed.error.format())}`);
103+
}
104+
}
105+
106+
return await prisma.event.create({
107+
data: {
108+
type,
109+
userId,
110+
data: (data ?? {}) as Prisma.InputJsonValue,
111+
},
112+
});
113+
} catch (error) {
114+
logger.error('createEvent error:', error);
115+
throw error;
116+
}
117+
}
118+
119+
const defaultSortKey = 'createdAt';
120+
61121
type SearchEvent = Prisma.EventGetPayload<{
62122
select: {
63123
id: true;
@@ -76,35 +136,6 @@ type SearchEvent = Prisma.EventGetPayload<{
76136
};
77137
}>;
78138

79-
const defaultSortKey = 'createdAt';
80-
81-
const validationKeys = Object.keys(validationSchemas);
82-
83-
export async function createEvent(type: EventType, userId = '', data = {}) {
84-
try {
85-
if (validationKeys.includes(type)) {
86-
const validationSchame = validationSchemas[type as keyof typeof validationSchemas];
87-
88-
const parsed = validationSchame.safeParse(data);
89-
if (!parsed.success) {
90-
throw Error(`invalid data for event type ${type}: ${JSON.stringify(data)}`);
91-
}
92-
}
93-
94-
const event = await prisma.event.create({
95-
data: {
96-
type,
97-
userId,
98-
data,
99-
},
100-
});
101-
102-
return event;
103-
} catch (error) {
104-
logger.error('createEvent:', error);
105-
}
106-
}
107-
108139
export async function searchEvents({
109140
types = [],
110141
dates = [],

docs/development-setup/local-development-environment.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ git config gpg.program gpg
6161

6262
1. Install `asdf` according to the `asdf` installation guide.
6363
- https://asdf-vm.com/guide/getting-started.html#getting-started
64+
- example
65+
```sh
66+
curl -fsSL -o asdf.tar.gz "https://github.com/asdf-vm/asdf/releases/download/v0.19.0/asdf-v0.19.0-linux-amd64.tar.gz" || { echo "❌ Failed to download ASDF" }
67+
tar -xzf asdf.tar.gz
68+
rm -f asdf.tar.gz
69+
mv asdf /usr/local/bin/
70+
echo 'export PATH="/usr/local/bin/:${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH"' >> ~/.bashrc # or ~/.zshrc, ~/.bash_profile, etc..
71+
```
6472
1. Install `asdf` packages defined in `.tool-versions`.
6573
```sh
6674
cat .tool-versions | cut -f 1 -d ' ' | xargs -n 1 asdf plugin-add || true

sandbox/keycloak-provision/main.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -115,41 +115,6 @@ async function main() {
115115
clientScopeId: scope?.id as string,
116116
});
117117

118-
// Create ministry level roles
119-
[
120-
'aest',
121-
'ag',
122-
'agri',
123-
'alc',
124-
'bcpc',
125-
'citz',
126-
'dbc',
127-
'eao',
128-
'educ',
129-
'emcr',
130-
'empr',
131-
'env',
132-
'fin',
133-
'flnr',
134-
'hlth',
135-
'irr',
136-
'jedc',
137-
'lbr',
138-
'ldb',
139-
'mcf',
140-
'mmha',
141-
'psa',
142-
'pssg',
143-
'sdpr',
144-
'tca',
145-
'tran',
146-
'hma',
147-
'wlrs',
148-
].forEach(async (ministry) => {
149-
await kc.createClientRole(AUTH_REALM_NAME, authClient?.id as string, `ministry-${ministry}-reader`);
150-
await kc.createClientRole(AUTH_REALM_NAME, authClient?.id as string, `ministry-${ministry}-editor`);
151-
});
152-
153118
await kc.createClientRole(AUTH_REALM_NAME, authClient?.id as string, `billing-reviewer`);
154119
await kc.createClientRole(AUTH_REALM_NAME, authClient?.id as string, `billing-reader`);
155120
await kc.createClientRole(AUTH_REALM_NAME, authClient?.id as string, `finance-manager`);

0 commit comments

Comments
 (0)