Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ORGANIZATION_SERVICE_URL="http://localhost:4004"
HR_SERVICE_URL="http://localhost:4005"
DOCS_OAUTH_CLIENT_ID="00000003-0000-4000-8000-130000000002"
ALLOWED_SERVICE_ORIGINS='["http://localhost:4001","http://localhost:4002","http://localhost:4003","http://localhost:4004","http://localhost:4005"]'
ALLOWED_UI_ORIGINS='[]'
ALLOWED_UI_ORIGINS='["http://localhost:3000"]'

# ---------------------------------------------------------------------------
# OpenTelemetry
Expand Down
2 changes: 1 addition & 1 deletion .env.stand.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ORGANIZATION_SERVICE_URL="http://localhost:4004"
HR_SERVICE_URL="http://localhost:4005"
DOCS_OAUTH_CLIENT_ID="00000003-0000-4000-8000-130000000002"
ALLOWED_SERVICE_ORIGINS='["http://localhost:4001","http://localhost:4002","http://localhost:4003","http://localhost:4004","http://localhost:4005"]'
ALLOWED_UI_ORIGINS='[]'
ALLOWED_UI_ORIGINS='["http://localhost:3000"]'

# ---------------------------------------------------------------------------
# OpenTelemetry
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Platform | Notifications: email via AWS SES, SMS via AWS SNS, in-app notifications, recipient preferences, dispatch retries and message status tracking.",
"author": "Daniil Egorov (https://github.com/Lambdius)",
"license": "SEE LICENSE IN LICENSE",
"version": "1.0.6",
"version": "1.0.7",
"main": "dist/index.js",
"engines": {
"node": "24.18.0"
Expand Down Expand Up @@ -69,7 +69,7 @@
"@mikro-orm/nestjs": "7.0.2",
"@mikro-orm/postgresql": "7.1.13",
"@mikro-orm/seeder": "7.1.13",
"@monadiam/shared": "github:MonadIAM/shared#v3.1.0",
"@monadiam/shared": "github:MonadIAM/shared#v3.1.1",
"@nest-lab/throttler-storage-redis": "1.2.0",
"@nestjs/bullmq": "11.0.5",
"@nestjs/common": "11.2.1",
Expand Down
15 changes: 9 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ color: true
loglevel: info
savePrefix: ""
allowBuilds:
"@monadiam/shared@https://codeload.github.com/MonadIAM/shared/tar.gz/5cb719bd74338d05a5c25961ea864605b89a7864": true
"@monadiam/shared@https://codeload.github.com/MonadIAM/shared/tar.gz/e4bdb27ad3ff42798bf8a57ad7789baca4e04664": true
"@launchql/protobufjs": true
"msgpackr-extract": true
"unrs-resolver": true
Expand Down
25 changes: 21 additions & 4 deletions src/context/application/queries/notification.contract.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { QueryMode } from "~context/enums";
import { QueryMode, ChannelType } from "~context/enums";

declare global {
namespace Queries.Notification {
interface Contract extends ControllerContract {}
type Data = Entities.Notification & {
message: Nullable<Entities.Message>;
duplicatedTo: ChannelType[];
};

interface Contract extends InternalContract, ControllerContract {}

interface InternalContract {
mapNotification: MapNotification.Signature;
}

namespace MapNotification {
type Props = { notification: Entities.Notification };

type Result = Data;

type Signature = (props: Props) => Result;
}

interface ControllerContract {
countUnread: CountUnread.Signature;
Expand Down Expand Up @@ -32,7 +49,7 @@ declare global {

type Props = DefaultProps | ManageProps;

type Result = Promise<Entities.Notification>;
type Result = Promise<Data>;

type Signature = (props: Props) => Result;
}
Expand All @@ -55,7 +72,7 @@ declare global {

type Props = DefaultProps | ManageProps;

type Result = Promise<[Entities.Notification[], number]>;
type Result = Promise<[Data[], number]>;

type Signature = (props: Props) => Result;
}
Expand Down
40 changes: 36 additions & 4 deletions src/context/application/queries/notification.queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable, Scope } from "@nestjs/common";
import { LoadStrategy } from "@mikro-orm/core";

import { NOTIFICATION_REPOSITORY } from "~context/infrastructure/repositories";
import { ChannelType, MessageStatus, QueryMode } from "~context/enums";
Expand Down Expand Up @@ -29,7 +30,12 @@ export class NotificationQueries implements Queries.Notification.Contract {
return { count };
}

public findUnique(props: Queries.Notification.FindUnique.Props): Queries.Notification.FindUnique.Result {
public async findUnique(props: Queries.Notification.FindUnique.Props): Queries.Notification.FindUnique.Result {
const options: ORM.FindOptions<Entities.Notification, "messages"> = {
strategy: LoadStrategy.JOINED,
populate: ["messages"],
};

const where: ORM.FilterQuery<Entities.Notification> = {
id: props.notification,
};
Expand All @@ -45,10 +51,17 @@ export class NotificationQueries implements Queries.Notification.Contract {
};
}

return this.notificationRepository.findUniqueOrThrow({ where });
const notification = await this.notificationRepository.findUniqueOrThrow({ where, options });

return this.mapNotification({ notification });
}

public findMany(props: Queries.Notification.FindMany.Props): Queries.Notification.FindMany.Result {
public async findMany(props: Queries.Notification.FindMany.Props): Queries.Notification.FindMany.Result {
const options: ORM.FindOptions<Entities.Notification, "messages"> = {
strategy: LoadStrategy.JOINED,
populate: ["messages"],
};

const prefilter: ORM.ObjectQuery<Entities.Notification> = {};

if (props.mode === QueryMode.DEFAULT) {
Expand All @@ -62,11 +75,30 @@ export class NotificationQueries implements Queries.Notification.Contract {
};
}

return this.notificationRepository.findMany({
const [notifications, count] = await this.notificationRepository.findMany({
pagination: props.pagination,
filters: props.filters,
sort: props.sort,
prefilter,
options,
});

return [notifications.map((notification) => this.mapNotification({ notification })), count];
}

public mapNotification({
notification,
}: Queries.Notification.MapNotification.Props): Queries.Notification.MapNotification.Result {
const result: Queries.Notification.Data = { ...notification, message: null, duplicatedTo: [] };

for (const message of notification.messages.getItems()) {
if (message.channelType === ChannelType.IN_APP) {
result.message = message;
} else if (message.status === MessageStatus.DELIVERED) {
result.duplicatedTo.push(message.channelType);
}
}

return result;
}
}
6 changes: 5 additions & 1 deletion src/context/domain/services/message.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable, Scope } from "@nestjs/common";
import { LockMode } from "@mikro-orm/core";

import { MESSAGE_REPOSITORY } from "~context/infrastructure/repositories";
import { DISPATCH_DELAY_QUEUE } from "~context/infrastructure/queues/tokens";
Expand Down Expand Up @@ -49,7 +50,10 @@ export class MessageService implements Services.Message.Contract {
public async markRead(props: Services.Message.MarkRead.Props): Services.Message.MarkRead.Result {
const { transaction, input } = props;
const message = await this.messageRepository.findUniqueOrThrow({
options: { populate: ["notification", "notification.recipient"] },
options: {
populate: ["notification", "notification.recipient"],
lockMode: LockMode.PESSIMISTIC_WRITE,
},
where: { id: input.message },
transaction,
});
Expand Down
6 changes: 5 additions & 1 deletion src/context/domain/services/message.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import { LockMode } from "@mikro-orm/core";

import { MessageUnitHelpers } from "~testing/unit/domain-service/message.helpers";
import { FailureReason } from "~context/enums";
Expand Down Expand Up @@ -109,7 +110,10 @@ describe("[DomainService] - Message", () => {

// Assert
expect(result).toHaveBeenCalledWith({
options: { populate: ["notification", "notification.recipient"] },
options: {
populate: ["notification", "notification.recipient"],
lockMode: LockMode.PESSIMISTIC_WRITE,
},
transaction: transaction.entityManager,
where: { id: MESSAGE_ID },
});
Expand Down
16 changes: 14 additions & 2 deletions src/context/infrastructure/mappers/notification.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { QueryOrder } from "@mikro-orm/postgresql";

import { NotificationCategory, PlatformService } from "~context/enums";
import { NotificationCategory, PlatformService, ChannelType } from "~context/enums";
import { ORMAdapter } from "~infrastructure/database/utils";

export class NotificationMapper implements Repositories.Mappers.Contract<
Expand Down Expand Up @@ -31,6 +31,18 @@ export class NotificationMapper implements Repositories.Mappers.Contract<
if (filters.createdAt) {
where.createdAt = ORMAdapter.applyOrdinalFilter<Date>(filters.createdAt);
}
if (filters.isRead !== undefined) {
where.$and = [
{
messages: {
$some: {
readAt: filters.isRead ? { $ne: null } : null,
channelType: ChannelType.IN_APP,
},
},
},
];
}

return where;
}
Expand All @@ -42,7 +54,7 @@ export class NotificationMapper implements Repositories.Mappers.Contract<
): ORM.FindOptions<Entities.Notification, P, F> {
return {
...basic,
orderBy: ORMAdapter.orderBy<Entities.Notification>(sort, { createdAt: QueryOrder.ASC }),
orderBy: ORMAdapter.orderBy<Entities.Notification>(sort, { createdAt: QueryOrder.DESC, id: QueryOrder.DESC }),
...ORMAdapter.pagination(pagination),
};
}
Expand Down
1 change: 1 addition & 0 deletions src/context/infrastructure/mappers/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ declare global {
realm?: LinkFilterDTO;
sourceService?: StringFilterDTO;
createdAt?: OrdinalFilterDTO<Date>;
isRead?: boolean;
};

type Sort = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ describe("[Repository] - Message", () => {
notification,
channel,
});
const otherNotification = await suite.fixtures().createNotification({ recipient });
await suite.fixtures().createMessage({
channelType: ChannelType.EMAIL,
notification,
notification: otherNotification,
channel,
});

Expand Down
7 changes: 7 additions & 0 deletions src/context/infrastructure/schemas/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export const MessageSchema = new EntitySchema<Message>({
tableName: "message",
schema: "notification",

uniques: [
{
name: "message_notification_channel_type_unique",
properties: ["notification", "channelType"],
},
],

indexes: [
{
name: "message_notification_idx",
Expand Down
19 changes: 17 additions & 2 deletions src/context/interface/dto/notification/res/entity.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { Expose, Type } from "class-transformer";

import { NotificationCategory, PlatformService } from "~context/enums";
import { NotificationCategory, PlatformService, ChannelType } from "~context/enums";
import { Validator } from "~common/validator";

import { MessageDTO } from "../../message";

@ApiSchema({ name: "Notification" })
export class NotificationDTO {
@Expose()
Expand Down Expand Up @@ -42,4 +44,17 @@ export class NotificationDTO {
@Validator.IsDate()
@ApiProperty({ required: true, type: Date })
declare public createdAt: Date;

@Expose()
@Type(() => MessageDTO)
@Validator.IsOptional()
@Validator.ValidateNested()
@ApiProperty({ required: true, type: MessageDTO, nullable: true })
declare public message: Nullable<MessageDTO>;

@Expose()
@Validator.IsArray()
@Validator.IsEnum(ChannelType, { each: true })
@ApiProperty({ required: true, enum: ChannelType, enumName: "ChannelType", isArray: true })
declare public duplicatedTo: ChannelType[];
}
6 changes: 6 additions & 0 deletions src/context/interface/dto/notification/utils/filters.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export class DefaultFiltersDTO implements Omit<Repositories.Mappers.Notification
@ApiProperty({ required: false, type: StringFilterDTO })
public sourceService?: StringFilterDTO;

@Expose()
@Validator.IsBoolean()
@Validator.IsOptional()
@ApiProperty({ required: false, type: Boolean })
public isRead?: boolean;

@Expose()
@Validator.IsOptional()
@Validator.ValidateNested()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,17 @@
"primary": false,
"unique": false
},
{
"columnNames": [
"notification_id",
"channel_type"
],
"composite": true,
"constraint": true,
"keyName": "message_notification_channel_type_unique",
"primary": false,
"unique": true
},
{
"columnNames": [
"id"
Expand Down
Loading
Loading