Skip to content

Commit 3adad58

Browse files
committed
Implement notification queries for EdgeDB
1 parent 90dd13c commit 3adad58

File tree

6 files changed

+177
-3
lines changed

6 files changed

+177
-3
lines changed

src/components/comments/mention-notification/comment-via-mention-notification.strategy.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { node, Query, relation } from 'cypher-query-builder';
22
import { createRelationships, exp } from '~/core/database/query';
3+
import { e } from '~/core/edgedb';
34
import {
45
INotificationStrategy,
56
InputOf,
@@ -18,6 +19,12 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy
1819
);
1920
}
2021

22+
insertForEdgeDB(input: InputOf<Mentioned>) {
23+
return e.insert(e.Notification.CommentMentioned, {
24+
comment: e.cast(e.Comments.Comment, e.uuid(input.comment)),
25+
});
26+
}
27+
2128
hydrateExtraForNeo4j(outVar: string) {
2229
return (query: Query) =>
2330
query
@@ -32,4 +39,10 @@ export class CommentViaMentionNotificationStrategy extends INotificationStrategy
3239
}).as(outVar),
3340
);
3441
}
42+
43+
hydrateExtraForEdgeDB() {
44+
return e.is(e.Notification.CommentMentioned, {
45+
comment: true,
46+
});
47+
}
3548
}

src/components/notification-system/system-notification.strategy.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { node, Query } from 'cypher-query-builder';
2+
import { e } from '~/core/edgedb';
23
import { INotificationStrategy, NotificationStrategy } from '../notifications';
34
import { SystemNotification } from './system-notification.dto';
45

@@ -8,4 +9,20 @@ export class SystemNotificationStrategy extends INotificationStrategy<SystemNoti
89
return (query: Query) =>
910
query.match(node('recipient', 'User')).return('recipient');
1011
}
12+
13+
recipientsForEdgeDB() {
14+
return e.User; // all users
15+
}
16+
17+
insertForEdgeDB(input: SystemNotification) {
18+
return e.insert(e.Notification.System, {
19+
message: input.message,
20+
});
21+
}
22+
23+
hydrateExtraForEdgeDB() {
24+
return e.is(e.Notification.System, {
25+
message: true,
26+
});
27+
}
1128
}

src/components/notifications/dto/notification.dto.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { Field, InterfaceType } from '@nestjs/graphql';
22
import { DateTime } from 'luxon';
33
import { keys as keysOf } from 'ts-transformer-keys';
4-
import { DateTimeField, Resource, SecuredProps } from '~/common';
4+
import {
5+
DateTimeField,
6+
resolveByTypename,
7+
Resource,
8+
SecuredProps,
9+
} from '~/common';
510
import { e } from '~/core/edgedb';
611
import { RegisterResource } from '~/core/resources';
712

813
@RegisterResource({ db: e.default.Notification })
914
@InterfaceType({
1015
implements: [Resource],
16+
resolveType: resolveByTypename(Notification.name),
1117
})
1218
export class Notification extends Resource {
1319
static readonly Props: string[] = keysOf<Notification>();
@@ -18,6 +24,8 @@ export class Notification extends Resource {
1824
})
1925
readonly unread: boolean;
2026

27+
declare readonly __typename: string;
28+
2129
@DateTimeField({
2230
nullable: true,
2331
description: 'When the notification was read for the requesting user',
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { forwardRef, Inject, Injectable } from '@nestjs/common';
2+
import { Nil } from '@seedcompany/common';
3+
import { ID, PublicOf, ResourceShape } from '~/common';
4+
import { e, RepoFor, ScopeOf } from '~/core/edgedb';
5+
import {
6+
MarkNotificationReadArgs,
7+
Notification,
8+
NotificationListInput,
9+
} from './dto';
10+
import { NotificationRepository as Neo4jRepository } from './notification.repository';
11+
import { NotificationServiceImpl } from './notification.service';
12+
13+
@Injectable()
14+
export class NotificationRepository
15+
extends RepoFor(Notification, {
16+
hydrate: (notification) => ({
17+
__typename: notification.__type__.name,
18+
...notification['*'],
19+
}),
20+
omit: ['create', 'update', 'list', 'readOne', 'readMany'],
21+
})
22+
implements PublicOf<Neo4jRepository>
23+
{
24+
constructor(
25+
@Inject(forwardRef(() => NotificationServiceImpl))
26+
private readonly service: NotificationServiceImpl & {},
27+
) {
28+
super();
29+
}
30+
31+
onModuleInit() {
32+
(this as any).hydrate = e.shape(e.Notification, (notification) => {
33+
return Object.assign(
34+
{
35+
__typename: notification.__type__.name,
36+
},
37+
notification['*'],
38+
...[...this.service.strategyMap.values()].flatMap(
39+
(strategy) => strategy.hydrateExtraForEdgeDB() ?? [],
40+
),
41+
);
42+
});
43+
}
44+
45+
async create(
46+
recipients: ReadonlyArray<ID<'User'>> | Nil,
47+
type: ResourceShape<any>,
48+
input: Record<string, any>,
49+
) {
50+
const strategy = this.service.getStrategy(type);
51+
52+
const created = strategy.insertForEdgeDB(input);
53+
54+
const recipientsQuery = recipients
55+
? e.cast(e.User, e.cast(e.uuid, e.set(...recipients)))
56+
: strategy.recipientsForEdgeDB(input);
57+
58+
const insertedRecipients = e.for(recipientsQuery, (user) =>
59+
e.insert(e.Notification.Recipient, {
60+
notification: created,
61+
user: user,
62+
}),
63+
);
64+
65+
const query = e.select({
66+
dto: e.select(created, this.hydrate),
67+
totalRecipients: e.count(insertedRecipients),
68+
});
69+
70+
return await this.db.run(query);
71+
}
72+
73+
async markRead({ id, unread }: MarkNotificationReadArgs) {
74+
const notification = e.cast(e.Notification, e.uuid(id));
75+
const next = unread ? null : e.datetime_of_transaction();
76+
77+
const recipient = e.assert_exists(notification.currentRecipient);
78+
const updated = e.update(recipient, () => ({
79+
set: { readAt: next },
80+
}));
81+
const query = e.select(updated.notification, this.hydrate);
82+
return await this.db.run(query);
83+
}
84+
85+
async list(input: NotificationListInput) {
86+
const myNotifications = e.select(e.Notification, (notification) => ({
87+
filter: e.op(e.global.currentUser, 'in', notification.recipients.user),
88+
}));
89+
90+
const paginated = this.paginate(myNotifications, input);
91+
92+
const myUnread = e.select(myNotifications, (notification) => ({
93+
filter: e.op(notification.unread, '=', true),
94+
}));
95+
96+
const query = e.select({
97+
...paginated,
98+
items: e.select(paginated.items as typeof e.Notification, this.hydrate),
99+
totalUnread: e.count(myUnread),
100+
});
101+
102+
return await this.db.run(query);
103+
}
104+
105+
protected listFilters(
106+
notification: ScopeOf<typeof e.Notification>,
107+
{ filter }: NotificationListInput,
108+
) {
109+
return [
110+
filter?.unread != null && e.op(notification.unread, '=', filter.unread),
111+
];
112+
}
113+
}

src/components/notifications/notification.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Module } from '@nestjs/common';
2-
import { NotificationRepository } from './notification.repository';
2+
import { splitDb } from '~/core';
3+
import { NotificationRepository as EdgeDBRepository } from './notification.edgedb.repository';
4+
import { NotificationRepository as Neo4jRepository } from './notification.repository';
35
import { NotificationResolver } from './notification.resolver';
46
import {
57
NotificationService,
@@ -11,7 +13,7 @@ import {
1113
NotificationResolver,
1214
{ provide: NotificationService, useExisting: NotificationServiceImpl },
1315
NotificationServiceImpl,
14-
NotificationRepository,
16+
splitDb(Neo4jRepository, EdgeDBRepository),
1517
],
1618
exports: [NotificationService],
1719
})

src/components/notifications/notification.strategy.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AbstractClass, Simplify } from 'type-fest';
44
import type { UnwrapSecured } from '~/common';
55
import type { RawChangeOf } from '~/core/database/changes';
66
import type { QueryFragment } from '~/core/database/query-augmentation/apply';
7+
import { $, e } from '~/core/edgedb';
78
import type { Notification } from './dto';
89

910
export const NotificationStrategy = createMetadataDecorator({
@@ -26,15 +27,35 @@ export abstract class INotificationStrategy<
2627
*/
2728
// eslint-disable-next-line @seedcompany/no-unused-vars
2829
recipientsForNeo4j(input: TInput) {
30+
// No recipients. Only those explicitly specified in the service create call.
2931
return (query: Query) => query.unwind([], 'recipient').return('recipient');
3032
}
3133

34+
recipientsForEdgeDB(
35+
// eslint-disable-next-line @seedcompany/no-unused-vars
36+
input: TInput,
37+
): $.Expression<$.TypeSet<typeof e.User.__element__>> {
38+
// No recipients. Only those explicitly specified in the service create call.
39+
return e.cast(e.User, e.set());
40+
}
41+
3242
saveForNeo4j(input: TInput) {
3343
return (query: Query) => query.setValues({ node: input }, true);
3444
}
3545

46+
abstract insertForEdgeDB(
47+
input: TInput,
48+
): $.Expression<
49+
$.TypeSet<
50+
$.ObjectType<string, typeof e.Notification.__element__.__pointers__>,
51+
$.Cardinality.One
52+
>
53+
>;
54+
3655
hydrateExtraForNeo4j(outVar: string): QueryFragment | undefined {
3756
const _used = outVar;
3857
return undefined;
3958
}
59+
60+
abstract hydrateExtraForEdgeDB(): Record<string, any>;
4061
}

0 commit comments

Comments
 (0)