Skip to content

Commit 0f970cd

Browse files
author
Andre Turner
committed
engagement repos and service file changes
1 parent d82bd53 commit 0f970cd

File tree

4 files changed

+225
-21
lines changed

4 files changed

+225
-21
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { inArray, node, Query, relation } from 'cypher-query-builder';
3+
import { ID, Order, PublicOf, Session, UnsecuredDto } from '~/common';
4+
import { DtoRepository } from '~/core/database';
5+
import {
6+
ACTIVE,
7+
createNode,
8+
createRelationships,
9+
INACTIVE,
10+
merge,
11+
requestingUser,
12+
sorting,
13+
} from '~/core/database/query';
14+
import { EngagementStatus, IEngagement } from '../dto';
15+
import {
16+
ExecuteEngagementTransitionInput,
17+
EngagementWorkflowEvent as WorkflowEvent,
18+
} from './dto';
19+
import { EngagementWorkflowRepository } from './engagement-workflow.repository';
20+
21+
@Injectable()
22+
export class EngagementWorkflowNeo4jRepository
23+
extends DtoRepository(WorkflowEvent)
24+
implements PublicOf<EngagementWorkflowRepository>
25+
{
26+
// @ts-expect-error It doesn't have match base signature
27+
async readMany(ids: readonly ID[], session: Session) {
28+
return await this.db
29+
.query()
30+
.apply(this.matchEvent())
31+
.where({ 'node.id': inArray(ids) })
32+
.apply(this.privileges.forUser(session).filterToReadable())
33+
.apply(this.hydrate())
34+
.map('dto')
35+
.run();
36+
}
37+
38+
async list(engagementId: ID, session: Session) {
39+
return await this.db
40+
.query()
41+
.apply(this.matchEvent())
42+
.where({ 'engagement.id': engagementId })
43+
.match(requestingUser(session))
44+
.apply(this.privileges.forUser(session).filterToReadable())
45+
.apply(sorting(WorkflowEvent, { sort: 'createdAt', order: Order.ASC }))
46+
.apply(this.hydrate())
47+
.map('dto')
48+
.run();
49+
}
50+
51+
protected matchEvent() {
52+
return (query: Query) =>
53+
query.match([
54+
node('node', this.resource.dbLabel),
55+
relation('in', '', ACTIVE),
56+
node('engagement', 'Engagement'),
57+
]);
58+
}
59+
60+
protected hydrate() {
61+
return (query: Query) =>
62+
query
63+
.match([
64+
node('engagement', 'Engagement'),
65+
relation('out', '', 'workflowEvent', ACTIVE),
66+
node('node'),
67+
relation('out', undefined, 'who'),
68+
node('who', 'User'),
69+
])
70+
.return<{ dto: UnsecuredDto<WorkflowEvent> }>(
71+
merge('node', {
72+
at: 'node.createdAt',
73+
who: 'who { .id }',
74+
engagement: 'engagement { .id }',
75+
}).as('dto'),
76+
);
77+
}
78+
79+
async recordEvent(
80+
{
81+
engagement,
82+
...props
83+
}: Omit<ExecuteEngagementTransitionInput, 'bypassTo'> & {
84+
to: EngagementStatus;
85+
},
86+
session: Session,
87+
) {
88+
const result = await this.db
89+
.query()
90+
.apply(
91+
await createNode(WorkflowEvent, {
92+
baseNodeProps: props,
93+
}),
94+
)
95+
.apply(
96+
createRelationships(WorkflowEvent, {
97+
in: { workflowEvent: ['Engagement', engagement] },
98+
out: { who: ['User', session.userId] },
99+
}),
100+
)
101+
.apply(this.hydrate())
102+
.first();
103+
const event = result!.dto;
104+
105+
await this.db.updateProperties({
106+
type: IEngagement,
107+
object: { id: engagement },
108+
changes: { status: event.to, statusModifiedAt: event.at },
109+
permanentAfter: null,
110+
});
111+
112+
return event;
113+
}
114+
115+
async mostRecentStep(
116+
engagementId: ID<'Engagement'>,
117+
steps: readonly EngagementStatus[],
118+
) {
119+
const result = await this.db
120+
.query()
121+
.match([
122+
node('node', 'Engagement', { id: engagementId }),
123+
relation('out', '', 'step', INACTIVE),
124+
node('prop'),
125+
])
126+
.where({ 'prop.value': inArray(steps) })
127+
.with('prop')
128+
.orderBy('prop.createdAt', 'DESC')
129+
.return<{ step: EngagementStatus }>(`prop.value as step`)
130+
.first();
131+
return result?.step ?? null;
132+
}
133+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ID, Session, UnsecuredDto } from '~/common';
3+
import { e, edgeql, RepoFor } from '~/core/edgedb';
4+
import { EngagementStatus } from '../dto';
5+
import {
6+
EngagementWorkflowEvent,
7+
ExecuteEngagementTransitionInput,
8+
} from './dto';
9+
10+
@Injectable()
11+
export class EngagementWorkflowRepository extends RepoFor(
12+
EngagementWorkflowEvent,
13+
{
14+
hydrate: (event) => ({
15+
id: true,
16+
who: true,
17+
at: true,
18+
transition: event.transitionKey,
19+
to: true,
20+
notes: true,
21+
engagement: true,
22+
}),
23+
omit: ['list', 'create', 'update', 'delete', 'readMany'],
24+
},
25+
) {
26+
async readMany(ids: readonly ID[], _session: Session) {
27+
return await this.defaults.readMany(ids);
28+
}
29+
async list(engagementId: ID, _session: Session) {
30+
const engagement = e.cast(e.Engagement, e.uuid(engagementId));
31+
const query = e.select(engagement.workflowEvents, this.hydrate);
32+
return await this.db.run(query);
33+
}
34+
async recordEvent(
35+
input: Omit<ExecuteEngagementTransitionInput, 'bypassTo'> & {
36+
to: EngagementStatus;
37+
},
38+
_session: Session,
39+
): Promise<UnsecuredDto<EngagementWorkflowEvent>> {
40+
const engagement = e.cast(e.Engagement, e.uuid(input.engagement));
41+
const created = e.insert(e.Engagement.WorkflowEvent, {
42+
engagement,
43+
projectContext: engagement.projectContext,
44+
transitionKey: input.transition,
45+
to: input.to,
46+
notes: input.notes,
47+
});
48+
const query = e.select(created, this.hydrate);
49+
return await this.db.run(query);
50+
}
51+
async mostRecentStep(
52+
engagementId: ID<'Engagement'>,
53+
steps: readonly EngagementStatus[],
54+
) {
55+
const query = edgeql(`
56+
with
57+
engagement := <Engagement><uuid>$engagementId,
58+
steps := array_unpack(<array<Engagement::Status>>$steps),
59+
mostRecentEvent := (
60+
select engagement.workflowEvents
61+
filter .to in steps if exists steps else true
62+
order by .at desc
63+
limit 1
64+
)
65+
select mostRecentEvent.to
66+
`);
67+
return await this.db.run(query, { engagementId, steps });
68+
}
69+
}

src/components/engagement/workflow/engagement-workflow.service.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { ModuleRef } from '@nestjs/core';
3-
import { Session, UnsecuredDto } from '~/common';
3+
import { ID, Session, UnsecuredDto } from '~/common';
44
import { IEventBus, ResourceLoader } from '~/core';
55
import {
66
findTransition,
@@ -13,7 +13,7 @@ import {
1313
} from './dto';
1414
import { EngagementWorkflow } from './engagement-workflow';
1515
import { EngagementWorkflowRepository } from './engagement-workflow.repository';
16-
//import { EngagementTransitionedEvent } from './events/engagement-transitioned.event';
16+
import { EngagementTransitionedEvent } from './events/engagement-transitioned.event';
1717

1818
@Injectable()
1919
export class EngagementWorkflowService extends WorkflowService(
@@ -28,15 +28,18 @@ export class EngagementWorkflowService extends WorkflowService(
2828
super();
2929
}
3030

31-
// async list(report: Project, session: Session): Promise<WorkflowEvent[]> {
32-
// const dtos = await this.repo.list(report.id, session);
33-
// return dtos.map((dto) => this.secure(dto, session));
34-
// }
31+
async list(
32+
engagement: Engagement,
33+
session: Session,
34+
): Promise<WorkflowEvent[]> {
35+
const dtos = await this.repo.list(engagement.id, session);
36+
return dtos.map((dto) => this.secure(dto, session));
37+
}
3538

36-
// async readMany(ids: readonly ID[], session: Session) {
37-
// const dtos = await this.repo.readMany(ids, session);
38-
// return dtos.map((dto) => this.secure(dto, session));
39-
// }
39+
async readMany(ids: readonly ID[], session: Session) {
40+
const dtos = await this.repo.readMany(ids, session);
41+
return dtos.map((dto) => this.secure(dto, session));
42+
}
4043

4144
private secure(
4245
dto: UnsecuredDto<WorkflowEvent>,
@@ -100,13 +103,13 @@ export class EngagementWorkflowService extends WorkflowService(
100103
engagements.clear(loaderKey);
101104
const updated = await engagements.load(loaderKey);
102105

103-
// const event = new EngagementTransitionedEvent(
104-
// updated,
105-
// previous.status.value!,
106-
// next,
107-
// unsecuredEvent,
108-
// );
109-
// await this.eventBus.publish(event);
106+
const event = new EngagementTransitionedEvent(
107+
updated,
108+
previous.status.value!,
109+
next,
110+
unsecuredEvent,
111+
);
112+
await this.eventBus.publish(event);
110113

111114
return updated;
112115
}
@@ -121,8 +124,7 @@ export class EngagementWorkflowService extends WorkflowService(
121124
currentEngagement,
122125
session,
123126
);
124-
// Pick the first matching to step.
125-
// Lack of detail is one of the reasons why this is legacy logic.
127+
126128
const transition = transitions.find((t) => t.to === step);
127129

128130
await this.executeTransition(

src/components/engagement/workflow/engagement-workflow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EngagementWorkflowEvent } from './dto';
55
import {
66
BackTo,
77
BackToActive,
8-
ResolveParams,
8+
ResolveEngagementParams,
99
} from './transitions/dynamic-step';
1010

1111
// This also controls the order shown in the UI.
@@ -16,7 +16,7 @@ export const EngagementWorkflow = defineWorkflow({
1616
name: 'Engagement',
1717
states: Step,
1818
event: EngagementWorkflowEvent,
19-
context: defineContext<ResolveParams>,
19+
context: defineContext<ResolveEngagementParams>,
2020
})({
2121
Reject: {
2222
from: Step.InDevelopment,

0 commit comments

Comments
 (0)