Skip to content

Commit

Permalink
feat(api): experimenting with @nestjs/cqrs
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Apr 5, 2019
1 parent ddb088d commit c2b69f4
Show file tree
Hide file tree
Showing 27 changed files with 228 additions and 88 deletions.
39 changes: 39 additions & 0 deletions apps/api/src/app/commands/create-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../app.module';
import { cli } from 'cli-ux';
import { UserService } from '../user';

/**
* Create admin account using command line interface
*/
const main = async () => {
const app = await NestFactory.createApplicationContext(AppModule, {
logger: false,
});

const firstName = await cli.prompt('First name');
const lastName = await cli.prompt('Last name');
const email = await cli.prompt('Email');
const username = await cli.prompt('Username');
const password = await cli.prompt('Password', { type: 'mask' });

try {
cli.action.start('creating an admin');

await app.get(UserService).create({
firstName,
lastName,
email,
username,
});

cli.action.stop();
} catch (error) {
cli.action.stop();
console.error(error);
} finally {
await app.close();
}
};

main();
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NotificationsHandler } from './notifications.handler';
import { NotificationsDeleteHandler } from './notifications-delete.handler';
import { NotificationsMarkAsReadHandler } from './notifications-make-as-read.handler';

export const CommandHandlers = [NotificationsHandler, NotificationsDeleteHandler, NotificationsMarkAsReadHandler];
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { NotificationService } from '../../notification.service';
import { NotificationsDeleteCommand } from '../notifications-delete.command';

@CommandHandler(NotificationsDeleteCommand)
export class NotificationsDeleteHandler implements ICommandHandler<NotificationsDeleteCommand> {
constructor(private readonly notificationService: NotificationService) {}

public async execute(command: NotificationsDeleteCommand): Promise<void> {
console.log('command:DeleteNotificationCommand', command);
await this.notificationService.onDeleteNotification(command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NotificationService } from '../../notification.service';
import { NotificationsMarkAsReadCommand } from '../notifications-make-as-read.command';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

@CommandHandler(NotificationsMarkAsReadCommand)
export class NotificationsMarkAsReadHandler implements ICommandHandler<NotificationsMarkAsReadCommand> {
constructor(private readonly notificationService: NotificationService) {}

public async execute(command: NotificationsMarkAsReadCommand): Promise<void> {
console.log('command:NotificationsMarkAsReadCommand', command);
await this.notificationService.onMarkAsRead(command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NotificationService } from '../../notification.service';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { GenericCommand } from '../../../../shared';
import { NotificationsDeleteCommand } from '../notifications-delete.command';
import { NotificationsMarkAsReadCommand } from '../notifications-make-as-read.command';
import { Logger } from '@nestjs/common';

@CommandHandler(GenericCommand)
export class NotificationsHandler implements ICommandHandler<GenericCommand> {
private readonly logger = new Logger(NotificationsHandler.name);
constructor(private readonly notificationService: NotificationService) {}

public async execute(command: GenericCommand): Promise<void> {
const { type, payload, user } = command;
switch (type) {
case NotificationsDeleteCommand.type: {
return await this.notificationService.onMarkAsRead(new NotificationsDeleteCommand(payload, user));
}
case NotificationsMarkAsReadCommand.type: {
return await this.notificationService.onMarkAsRead(new NotificationsMarkAsReadCommand(payload, user));
}
default: {
this.logger.error('received unknown command: ', command.type);
// return this.commandBus.execute(command);
}
}
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/notifications/notification/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { NotificationsDeleteCommand } from './notifications-delete.command'
export { NotificationsMarkAsReadCommand } from './notifications-make-as-read.command'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ICommand } from '@nestjs/cqrs';
import { User } from '@ngx-starter-kit/models';

export class NotificationsDeleteCommand implements ICommand {
static readonly type = '[Notifications] Delete';
constructor(public readonly payload: any, public readonly user: User) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ICommand } from '@nestjs/cqrs';
import { User } from '@ngx-starter-kit/models';

export class NotificationsMarkAsReadCommand implements ICommand {
static readonly type = '[Notifications] MarkAsRead';
constructor(public readonly payload: any, public readonly user: User) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

export class SendNotificationDto {
@ApiModelProperty({ type: Number })
@ApiModelProperty({ type: String })
@IsNotEmpty()
id: number;
id: string;

@ApiModelPropertyOptional({ type: String })
@IsOptional()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { ApiExcludeEndpoint, ApiOAuth2Auth, ApiOperation, ApiResponse, ApiUseTag
import { Notification } from './notification.entity';
import { CreateNotificationDto } from './dto/create-notification.dto';
import { NotificationService } from './notification.service';
import { CurrentUser, Roles, RolesEnum, User } from '../../auth';
import { CurrentUser, Roles, RolesEnum } from '../../auth';
import { SendNotificationDto } from './dto/send-notification.dto';
import { UpdateNotificationDto } from './dto/update-notification.dto';
import { NotificationList } from './dto/notification-list.model';
import { FindNotificationsDto } from './dto/find-notifications.dto';
import { FindOwnNotificationsDto } from './dto/find-own-notifications.dto';
import { User } from '@ngx-starter-kit/models';
import { UUIDValidationPipe } from '../../shared';

@ApiOAuth2Auth(['read'])
@ApiUseTags('Notifications')
Expand All @@ -32,7 +34,7 @@ export class NotificationController extends CrudController<Notification> {
return this.notificationService.findAll(filter);
}

@ApiOperation({ title: 'find user\'s and global Notifications' })
@ApiOperation({ title: "find user's and global Notifications" })
@ApiResponse({ status: HttpStatus.OK, description: 'Find matching Notifications', type: NotificationList })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'No matching records found' })
@Get('own')
Expand All @@ -47,7 +49,7 @@ export class NotificationController extends CrudController<Notification> {
@ApiUseTags('Admin')
@Roles(RolesEnum.ADMIN)
@Get(':id')
async findById(@Param('id') id: string): Promise<Notification> {
async findById(@Param('id', UUIDValidationPipe) id: string): Promise<Notification> {
return super.findById(id);
}

Expand Down Expand Up @@ -90,7 +92,7 @@ export class NotificationController extends CrudController<Notification> {
@Roles(RolesEnum.ADMIN)
@Delete(':id')
async deleteByAdmin(@Param('id') id: string): Promise<any> {
return this.notificationService.update({ id: parseInt(id, 10) }, { isActive: false });
return this.notificationService.update(id, { isActive: false });
}

// @ApiOperation({ title: 'Delete record by user' })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Column, CreateDateColumn, Entity, Index, UpdateDateColumn, VersionColumn } from 'typeorm';
import { Base } from '../../core/entities/base.entity';
import { Base } from '../../core/entities/base';
import { ApiModelProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';

Expand Down
61 changes: 16 additions & 45 deletions apps/api/src/app/notifications/notification/notification.service.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,44 @@
import {
Injectable,
Logger,
OnApplicationBootstrap,
OnApplicationShutdown,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Any, In, Repository } from 'typeorm';
import { CrudService, IPagination } from '../../core';
import { Notification, TargetType } from './notification.entity';
import { EventBusGateway } from '../../shared';
import { DeleteNotification, MarkAsRead } from '../index';
import { User } from '../../auth';
import { CQRSGateway } from '../../shared';
import { SubscriptionService } from '../subscription/subscription.service';
import { PushService } from './push.service';
import { Subscription } from '../subscription/subscription.entity';
import { FindOwnNotificationsDto } from './dto/find-own-notifications.dto';
import { FindNotificationsDto } from './dto/find-notifications.dto';
import { User } from '@ngx-starter-kit/models';
import { NotificationsDeleteCommand, NotificationsMarkAsReadCommand } from './commands';

@Injectable()
export class NotificationService extends CrudService<Notification>
implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown {
export class NotificationService extends CrudService<Notification> implements OnModuleInit, OnModuleDestroy {
readonly logger = new Logger(NotificationService.name);
constructor(
private readonly pushService: PushService,
private readonly eventBus: EventBusGateway,
private readonly cqrsGateway: CQRSGateway,
private readonly subscriptionService: SubscriptionService,
@InjectRepository(Notification) private readonly notificationsRepository: Repository<Notification>,
@InjectRepository(Subscription) private readonly subscriptionRepository: Repository<Subscription>,
) {
super(notificationsRepository);
}

async onApplicationBootstrap(): Promise<void> {
// DO some async task
this.logger.log('in ApplicationBootstrap, done');
}
onModuleInit() {}

onApplicationShutdown(signal: string) {
console.log('in onApplicationShutdown, signal: ', signal); // e.g. "SIGINT"
// process.kill(process.pid, 'SIGINT');
}

onModuleInit() {
this.eventBus.on(MarkAsRead.type, this.onMarkAsRead.bind(this));
this.eventBus.on(DeleteNotification.type, this.onDeleteNotification.bind(this));
}

onModuleDestroy() {
this.eventBus.off(MarkAsRead.type, this.onMarkAsRead.bind(this));
this.eventBus.off(DeleteNotification.type, this.onDeleteNotification.bind(this));
}
onModuleDestroy() {}

async findAll({ take, skip, order, ...where }: FindNotificationsDto): Promise<IPagination<Notification>> {
return super.findAll({ where, take, skip, order });
}

async findOwn({ take, skip, order }: FindOwnNotificationsDto, actor: User): Promise<IPagination<Notification>> {
const criteria = new FindNotificationsDto({
target: In(['all', actor.username]),
isActive: true,
take,
skip,
order,
});
return super.findAll(criteria);
const where = { isActive: true, target: In(['all', actor.username]) };
return super.findAll({ where, take, skip, order });
}

async send(id: string | number) {
async send(id: string) {
const notification = await this.findOne(id);

const pushNotification = {
Expand Down Expand Up @@ -100,15 +70,16 @@ export class NotificationService extends CrudService<Notification>
});
}

async onMarkAsRead(action: MarkAsRead, user: User) {
async onMarkAsRead(command: NotificationsMarkAsReadCommand) {
await this.update(
{ id: parseInt(action.payload.id, 10), targetType: TargetType.USER, target: user.username },
{ id: command.payload.id, targetType: TargetType.USER, target: command.user.username },
{ read: true },
);
}
async onDeleteNotification(action: DeleteNotification, user: User) {

async onDeleteNotification(command: NotificationsDeleteCommand) {
await this.update(
{ id: parseInt(action.payload.id, 10), targetType: TargetType.USER, target: user.username },
{ id: command.payload.id, targetType: TargetType.USER, target: command.user.username },
{ isActive: false },
);
}
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/app/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CqrsModule } from '@nestjs/cqrs';
import { SharedModule } from '../shared';
import { NotificationController } from './notification/notification.controller';
import { NotificationService } from './notification/notification.service';
import { CommandHandlers } from './notification/commands/handlers';
import { SubscriptionController } from './subscription/subscription.controller';
import { SubscriptionService } from './subscription/subscription.service';
import { PushService } from './notification/push.service';
import { Notification } from './notification/notification.entity';
import { Subscription } from './subscription/subscription.entity';

@Module({
// imports: [SharedModule, TypeOrmModule.forFeature([NotificationsRepository])],
imports: [SharedModule, TypeOrmModule.forFeature([Notification, Subscription])],
providers: [PushService, SubscriptionService, NotificationService],
imports: [SharedModule, CqrsModule, TypeOrmModule.forFeature([Notification, Subscription])],
providers: [PushService, SubscriptionService, NotificationService, ...CommandHandlers],
controllers: [NotificationController, SubscriptionController],
})
export class NotificationsModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { CrudController } from '../../core';
import { Subscription } from './subscription.entity';
import { SubscriptionService } from './subscription.service';
import { CurrentUser, Roles, RolesEnum } from '../../auth/decorators';
import { User } from '../../auth';
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
import { UpdateSubscriptionDto } from './dto/update-subscription.dto';
import { SubscriptionList } from './dto/subscription-list.model';
import { FindSubscriptionsDto } from './dto/find-subscriptions.dto';
import { FindOwnSubscriptionsDto } from './dto/find-own-subscriptions.dto';
import { User } from '@ngx-starter-kit/models';

@ApiOAuth2Auth(['read'])
@ApiUseTags('Subscription')
Expand Down Expand Up @@ -88,7 +88,7 @@ export class SubscriptionController extends CrudController<Subscription> {
if (id.startsWith('http')) {
return this.subscriptionService.update({ endpoint: id, username: user.username }, entity);
} else {
return this.subscriptionService.update({ id: parseInt(id, 10), username: user.username }, entity);
return this.subscriptionService.update({ id, username: user.username }, entity);
}
}

Expand All @@ -100,7 +100,7 @@ export class SubscriptionController extends CrudController<Subscription> {
if (id.startsWith('http')) {
return this.subscriptionService.delete({ endpoint: id, username: user.username });
} else {
return this.subscriptionService.delete({ id: parseInt(id, 10), username: user.username });
return this.subscriptionService.delete({ id, username: user.username });
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Column, CreateDateColumn, Entity, Index, UpdateDateColumn, VersionColumn } from 'typeorm';
import { ApiModelProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { Base } from '../../core/entities/base.entity';
import { Base } from '../../core/entities/base';

@Entity('subscription')
export class Subscription extends Base {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Subscription } from './subscription.entity';
import { FindConditions, Repository } from 'typeorm';
import { setVapidDetails } from 'web-push';
import { environment as env } from '@env-api/environment';
import { User } from '../../auth';
import { FindOwnSubscriptionsDto } from './dto/find-own-subscriptions.dto';
import { FindSubscriptionsDto } from './dto/find-subscriptions.dto';
import { User } from '@ngx-starter-kit/models';

@Injectable()
export class SubscriptionService extends CrudService<Subscription> {
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/app/shared/commands/generic.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ICommand } from '@nestjs/cqrs';
import { User } from '@ngx-starter-kit/models';

export class GenericCommand implements ICommand {
constructor(public readonly type: string, public readonly payload: any, public readonly user: User) {}
}
Loading

0 comments on commit c2b69f4

Please sign in to comment.