Skip to content

Commit

Permalink
feat(api): added push API module to save PushSubscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Nov 20, 2018
1 parent 2e0769a commit 64d0d6c
Show file tree
Hide file tree
Showing 21 changed files with 217 additions and 29 deletions.
6 changes: 6 additions & 0 deletions PLAYBOOK-NEST.md
Expand Up @@ -101,6 +101,12 @@ nest g module notifications --dry-run
nest g controller notifications --dry-run
nest g service notifications notifications --dry-run
nest g class notification notifications --dry-run

# scaffold push module
nest g module push --dry-run
nest g controller push --dry-run
nest g service push --dry-run
nest g class subscription push --no-spec --dry-run # rename as subscription.entity.ts
```


Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/app.module.ts
Expand Up @@ -7,6 +7,7 @@ import { UserModule } from './user';
// import { ChatModule } from './chat';
import { AppController } from './app.controller';
import { NotificationsModule } from './notifications';
import { PushModule } from './push';

@Module({
imports: [
Expand All @@ -16,6 +17,7 @@ import { NotificationsModule } from './notifications';
children: [
{ path: '/auth', module: AuthModule },
{ path: '/user', module: UserModule },
{ path: '/push', module: PushModule },
// { path: '/account', module: AccountModule },
{ path: '/notifications', module: NotificationsModule },
],
Expand All @@ -26,6 +28,7 @@ import { NotificationsModule } from './notifications';
UserModule,
// AccountModule,
NotificationsModule,
PushModule,
// ChatModule,
],
controllers: [AppController],
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/core/core.module.ts
Expand Up @@ -10,6 +10,7 @@ import { ConnectionOptions } from 'typeorm';
import { Notification } from '../notifications/notification.entity';
import { User } from '../auth/user.entity';
import { environment as env } from '@env-api/environment';
import { Subscription } from '../push/subscription.entity';

@Module({
imports: [
Expand All @@ -19,7 +20,7 @@ import { environment as env } from '@env-api/environment';
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
...env.database,
entities: [Notification, User],
entities: [Notification, User, Subscription],
} as ConnectionOptions),
inject: [ConfigService],
}),
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/core/crud/crud.controller.ts
Expand Up @@ -31,7 +31,7 @@ export abstract class CrudController<T extends Base> {
description: 'Invalid input, The response body may contain clues as to what went wrong',
})
@Post()
async create(@Body() entity: DeepPartial<T>): Promise<T> {
async create(@Body() entity: DeepPartial<T>, options?: any): Promise<T> {
return this.crudService.create(entity);
}

Expand All @@ -43,15 +43,15 @@ export abstract class CrudController<T extends Base> {
description: 'Invalid input, The response body may contain clues as to what went wrong',
})
@Put(':id')
async update(@Param('id') id: string, @Body() entity: DeepPartial<T>): Promise<any> {
async update(@Param('id') id: string, @Body() entity: DeepPartial<T>, options?: any): Promise<any> {
return this.crudService.update(id, entity as any); // FIXME: https://github.com/typeorm/typeorm/issues/1544
}

@ApiOperation({ title: 'Delete record' })
@ApiResponse({ status: HttpStatus.NO_CONTENT, description: 'The record has been successfully deleted' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Record not found' })
@Delete(':id')
async delete(@Param('id') id: string): Promise<any> {
async delete(@Param('id') id: string, options?: any): Promise<any> {
return this.crudService.delete(id);
}
}
2 changes: 2 additions & 0 deletions apps/api/src/core/entities/audit-base.entity.ts
Expand Up @@ -12,10 +12,12 @@ export abstract class AuditBase {
@PrimaryGeneratedColumn()
id: number;

@ApiModelProperty({ type: Date })
// @Exclude()
@CreateDateColumn()
createdAt?: Date;

@ApiModelProperty({ type: Date })
// @Exclude()
@UpdateDateColumn()
updatedAt?: Date;
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/notifications/dto/create-notification.dto.ts
Expand Up @@ -3,7 +3,7 @@ import { IsAscii, IsBoolean, IsEnum, IsIn, IsNotEmpty, IsString, MaxLength, MinL
import { NotificationColor, NotificationIcon } from '../notification.entity';

export class CreateNotificationDto {
@ApiModelProperty({ type: String, enum: NotificationIcon, default: 'notifications' })
@ApiModelProperty({ type: String, enum: NotificationIcon, default: NotificationIcon.notifications })
@IsString()
@IsNotEmpty()
@IsEnum(NotificationIcon)
Expand All @@ -19,7 +19,7 @@ export class CreateNotificationDto {
@IsNotEmpty()
read: boolean = false;

@ApiModelProperty({ type: String, enum: NotificationColor })
@ApiModelProperty({ type: String, enum: NotificationColor, default: NotificationColor.PRIMARY })
@IsString()
@IsNotEmpty()
@IsIn(['warn', 'accent', 'primary'])
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/notifications/notification.entity.ts
Expand Up @@ -20,7 +20,7 @@ export enum NotificationIcon {

@Entity('notification')
export class Notification extends Base {
@ApiModelProperty({ type: String })
@ApiModelProperty({ type: String, enum: NotificationIcon, default: NotificationIcon.notifications })
@IsString()
@IsNotEmpty()
@Column()
Expand All @@ -43,10 +43,10 @@ export class Notification extends Base {
@Column()
read: boolean;

@ApiModelProperty({ type: String })
@ApiModelProperty({ type: String, enum: NotificationColor, default: NotificationColor.PRIMARY })
@IsString()
@IsNotEmpty()
@Column()
@Column({ enum: ['warn', 'accent', 'primary'] })
color?: NotificationColor;

@ApiModelProperty({ type: String, minLength: 8, maxLength: 20 })
Expand All @@ -62,6 +62,6 @@ export class Notification extends Base {
@IsBoolean()
@IsNotEmpty()
@Index()
@Column({ default: false})
@Column({ default: false })
native: boolean;
}
23 changes: 23 additions & 0 deletions apps/api/src/push/dto/create-subscription.dto.ts
@@ -0,0 +1,23 @@
import { ApiModelProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsUrl } from 'class-validator';

export class CreateSubscriptionDto {
@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsUrl({}, { message: 'endpoint must be a valid url.' })
endpoint: string;

@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsString()
auth: string;

@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsString()
p256dh: string;

@ApiModelProperty({ type: String, isArray: true })
@IsNotEmpty()
topics: string[];
}
1 change: 1 addition & 0 deletions apps/api/src/push/index.ts
@@ -0,0 +1 @@
export * from './push.module';
16 changes: 16 additions & 0 deletions apps/api/src/push/push.controller.spec.ts
@@ -0,0 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PushController } from './push.controller';

describe('Push Controller', () => {
let module: TestingModule;

beforeAll(async () => {
module = await Test.createTestingModule({
controllers: [PushController],
}).compile();
});
it('should be defined', () => {
const controller: PushController = module.get<PushController>(PushController);
expect(controller).toBeDefined();
});
});
31 changes: 31 additions & 0 deletions apps/api/src/push/push.controller.ts
@@ -0,0 +1,31 @@
import { Body, Controller, HttpStatus, Post } from '@nestjs/common';
import { ApiOAuth2Auth, ApiOperation, ApiResponse, ApiUseTags } from '@nestjs/swagger';
import { CrudController } from '../core';
import { Subscription } from './subscription.entity';
import { PushService } from './push.service';
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
import { CurrentUser, User } from '../auth';

@ApiOAuth2Auth(['read'])
@ApiUseTags('Sumo', 'Push')
@Controller()
export class PushController extends CrudController<Subscription> {
constructor(private readonly pushService: PushService) {
super(pushService);
}

@ApiOperation({ title: 'Create new record' })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'The record has been successfully created.',
type: Subscription,
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input, The response body may contain clues as to what went wrong',
})
@Post()
async create(@Body() entity: CreateSubscriptionDto, @CurrentUser() user: User): Promise<Subscription> {
return super.create({ ...entity, userId: user.userId });
}
}
12 changes: 12 additions & 0 deletions apps/api/src/push/push.module.ts
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PushController } from './push.controller';
import { PushService } from './push.service';
import { Subscription } from './subscription.entity';

@Module({
imports: [TypeOrmModule.forFeature([Subscription])],
providers: [PushService],
controllers: [PushController],
})
export class PushModule {}
16 changes: 16 additions & 0 deletions apps/api/src/push/push.service.spec.ts
@@ -0,0 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PushService } from './push.service';

describe('PushService', () => {
let service: PushService;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PushService],
}).compile();
service = module.get<PushService>(PushService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
12 changes: 12 additions & 0 deletions apps/api/src/push/push.service.ts
@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CrudService } from '../core';
import { Repository } from 'typeorm';
import { Subscription } from './subscription.entity';

@Injectable()
export class PushService extends CrudService<Subscription> {
constructor(@InjectRepository(Subscription) private readonly subscriptionRepository: Repository<Subscription>) {
super(subscriptionRepository);
}
}
52 changes: 52 additions & 0 deletions apps/api/src/push/subscription.entity.ts
@@ -0,0 +1,52 @@
import { Column, CreateDateColumn, Entity, Index, UpdateDateColumn, VersionColumn } from 'typeorm';
import { ApiModelProperty } from '@nestjs/swagger';
import { IsAscii, IsNotEmpty, IsString, IsUrl, MaxLength, MinLength } from 'class-validator';
import { Exclude } from 'class-transformer';
import { Base } from '../core/entities/base.entity';

@Entity('subscription')
export class Subscription extends Base {
@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsUrl({}, { message: 'endpoint must be a valid url.' })
@Index({ unique: true })
@Column()
endpoint: string;

@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsString()
@Column({})
auth: string;

@ApiModelProperty({ type: String })
@IsNotEmpty()
@IsString()
@Column({})
p256dh: string;

@ApiModelProperty({ type: String, minLength: 8, maxLength: 20 })
@IsAscii()
@IsNotEmpty()
@MinLength(8)
@MaxLength(20)
@Index()
@Column()
userId: string;

@ApiModelProperty({ type: String, isArray: true })
@Column('text', { array: true })
topics: string[];

@ApiModelProperty({ type: Date })
@CreateDateColumn()
createdAt?: Date;

@ApiModelProperty({ type: Date })
@UpdateDateColumn()
updatedAt?: Date;

@Exclude()
@VersionColumn()
version?: number;
}
33 changes: 23 additions & 10 deletions libs/core/src/lib/services/push-notification.service.ts
Expand Up @@ -2,37 +2,50 @@ import { Injectable } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { from as fromPromise, Observable, of } from 'rxjs';
import { environment } from '@env/environment';
import { take } from 'rxjs/operators';
// import {ApiService} from './api.service';

@Injectable({
providedIn: 'root',
})
export class PushNotificationService {
private pushSubscription: PushSubscription;

get available(): boolean {
return this.swPush.isEnabled;
}

constructor(private readonly swPush: SwPush /*private readonly apiService: ApiService*/) {}
constructor(private readonly swPush: SwPush /*private readonly apiService: ApiService*/) {
// subscribe for new messages for testing
this.swPush.messages.subscribe(message => {
console.log('received push notification', message);
});
}

async register() {
if (!this.swPush.isEnabled) {
if (!this.available) {
return;
}

// Key generation: https://web-push-codelab.glitch.me
const subscription = await this.swPush.requestSubscription({ serverPublicKey: environment.webPush.publicVapidKey });
console.log('Push subscription', subscription);
this.pushSubscription = subscription;
// return this.apiService.post('push/register', subscription).subscribe();
console.log('Push subscription created', subscription.toJSON());
// this.pushSubscription = subscription;
// this.apiService.post('api/push', subscription).subscribe();
}

unregister(): Observable<boolean> {
if (this.pushSubscription) {
return fromPromise(this.pushSubscription.unsubscribe());
async unregister() {
if (!this.available) {
return;
}

return of(true);
const subscription = await this.swPush.subscription.pipe(take(1)).toPromise();

if (subscription) {
console.log('deleting subscription', subscription.toJSON());
// this.apiService.delete('api/push', subscription).subscribe();
console.log('subscription deleted from database');
const res = await subscription.unsubscribe();
console.log('subscription deleted from local', res);
}
}
}
Expand Up @@ -33,7 +33,7 @@
</div>
</div>
<div class="mat-row">
<div class="mat-cell" fxFlex="30">Notifications</div>
<div class="mat-cell" fxFlex="30">Push Notifications</div>
<div class="mat-cell" fxFlex="70">
<mat-slide-toggle
aria-label="enableNotifications"
Expand Down
Expand Up @@ -51,12 +51,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.settingsForm
.get('enableNotifications')
.valueChanges.pipe(untilDestroy(this))
.subscribe(enableNotifications => {
.subscribe(async enableNotifications => {
if (enableNotifications) {
this.pnServ.register();
await this.pnServ.register();
this.store.dispatch(new EnableNotifications());
} else {
this.pnServ.unregister();
await this.pnServ.unregister();
this.store.dispatch(new DisableNotifications());
}
});
Expand Down

0 comments on commit 64d0d6c

Please sign in to comment.