-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add a WebSocket-based event gateway to the backend + create ent…
…ities for Demand and Device
- Loading branch information
1 parent
19841b7
commit af703ce
Showing
19 changed files
with
686 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
PORT=3030 | ||
BACKEND_PORT=3030 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { | ||
SubscribeMessage, | ||
WebSocketGateway, | ||
OnGatewayConnection, | ||
OnGatewayDisconnect, | ||
OnGatewayInit, | ||
MessageBody | ||
} from '@nestjs/websockets'; | ||
import { Logger } from '@nestjs/common'; | ||
import { getEventsServerPort } from '../port' | ||
|
||
import moment from 'moment'; | ||
import { SupportedEvents, IEvent, NewEvent } from '@energyweb/origin-backend-core'; | ||
|
||
const PORT = getEventsServerPort(); | ||
|
||
@WebSocketGateway(PORT, { transports: ['websocket'] }) | ||
export class EventsWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { | ||
|
||
private logger: Logger = new Logger('EventsWebSocketGateway'); | ||
private allEvents: IEvent[] = []; | ||
|
||
wsClients: any[] = []; | ||
|
||
afterInit() { | ||
this.logger.log(`Initialized the WebSockets server on port: ${PORT}.`); | ||
} | ||
|
||
handleConnection(client: any) { | ||
this.wsClients.push(client);; | ||
|
||
this.logger.log(`Client connected. Total clients connected: ${this.wsClients.length}`); | ||
} | ||
|
||
handleDisconnect(client: any) { | ||
for (let i = 0; i < this.wsClients.length; i++) { | ||
if (this.wsClients[i] === client) { | ||
this.wsClients.splice(i, 1); | ||
this.logger.log(`Client disconnected`); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private broadcastEvent(event: IEvent) { | ||
this.logger.log(`Broadcasting a new "${event.type}" event.`); | ||
|
||
const content = JSON.stringify(event); | ||
|
||
for (let client of this.wsClients) { | ||
client.send(content); | ||
} | ||
} | ||
|
||
@SubscribeMessage('getAllEvents') | ||
getAllEvents(client: any) { | ||
this.logger.log('Client requested getting all events.'); | ||
|
||
client.send(JSON.stringify(this.allEvents)); | ||
} | ||
|
||
@SubscribeMessage('events') | ||
handleEvent(@MessageBody() incomingEvent: NewEvent) { | ||
this.logger.log(`Incoming event: ${JSON.stringify(incomingEvent)}`); | ||
|
||
const { type, data } = incomingEvent; | ||
|
||
if (!type || !data) { | ||
return 'Incorrect event structure'; | ||
} | ||
|
||
const supportedEvents = Object.values(SupportedEvents); | ||
|
||
if (!supportedEvents.includes(type)) { | ||
return `Unsupported event name. Please use one of the following: ${supportedEvents.join(', ')}`; | ||
} | ||
|
||
const event: IEvent = { | ||
...incomingEvent, | ||
timestamp: moment().unix() | ||
}; | ||
|
||
this.allEvents.push(event); | ||
|
||
this.broadcastEvent(event); | ||
|
||
return `Saved ${type} event.`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { EventsWebSocketGateway } from './events.gateway'; | ||
|
||
@Module({ | ||
providers: [EventsWebSocketGateway], | ||
exports: [EventsWebSocketGateway] | ||
}) | ||
export class EventsModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
packages/origin-backend/src/pods/demand/demand.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import { Repository } from 'typeorm'; | ||
import { validate } from 'class-validator'; | ||
import { IDemand, DemandStatus, DemandPostData, DemandUpdateData, SupportedEvents, CreatedNewDemand, DemandPartiallyFilledEvent } from '@energyweb/origin-backend-core'; | ||
|
||
import { | ||
Controller, | ||
Get, | ||
Param, | ||
NotFoundException, | ||
Post, | ||
Body, | ||
UnprocessableEntityException, | ||
Delete, | ||
Put, | ||
Inject | ||
} from '@nestjs/common'; | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
|
||
import { Demand } from './demand.entity'; | ||
import { StorageErrors } from '../../enums/StorageErrors'; | ||
import { EventsWebSocketGateway } from '../../events/events.gateway'; | ||
|
||
@Controller('/Demand') | ||
export class DemandController { | ||
constructor( | ||
@InjectRepository(Demand) private readonly demandRepository: Repository<Demand>, | ||
@Inject(EventsWebSocketGateway) private readonly eventGateway: EventsWebSocketGateway | ||
) {} | ||
|
||
@Get() | ||
async getAll() { | ||
console.log(`<GET> Demand all`); | ||
|
||
const allDemands = await this.demandRepository.find(); | ||
|
||
for (let demand of allDemands) { | ||
demand.demandPartiallyFilledEvents = demand.demandPartiallyFilledEvents.map( | ||
event => JSON.parse(event) | ||
); | ||
} | ||
|
||
return allDemands; | ||
} | ||
|
||
@Get('/:id') | ||
async get(@Param('id') id: string) { | ||
const existing = await this.demandRepository.findOne(id, { | ||
loadRelationIds: true | ||
}); | ||
|
||
if (!existing) { | ||
throw new NotFoundException(StorageErrors.NON_EXISTENT); | ||
} | ||
|
||
existing.demandPartiallyFilledEvents = existing.demandPartiallyFilledEvents.map( | ||
event => JSON.parse(event) | ||
); | ||
|
||
return existing; | ||
} | ||
|
||
@Post() | ||
async post(@Body() body: DemandPostData) { | ||
let newEntity = new Demand(); | ||
|
||
const data: Omit<IDemand, 'id'> = { | ||
...body, | ||
status: DemandStatus.ACTIVE, | ||
demandPartiallyFilledEvents: [], | ||
location: body.location ?? [], | ||
deviceType: body.deviceType ?? [], | ||
otherGreenAttributes: body.otherGreenAttributes ?? '', | ||
typeOfPublicSupport: body.typeOfPublicSupport ?? '', | ||
registryCompliance: body.registryCompliance ?? '', | ||
procureFromSingleFacility: body.procureFromSingleFacility ?? false, | ||
vintage: body.vintage ?? [1900, 2100] | ||
}; | ||
|
||
Object.assign(newEntity, data); | ||
|
||
const validationErrors = await validate(newEntity); | ||
|
||
if (validationErrors.length > 0) { | ||
throw new UnprocessableEntityException({ | ||
success: false, | ||
errors: validationErrors | ||
}); | ||
} | ||
|
||
newEntity = await this.demandRepository.save(newEntity); | ||
|
||
const eventData: CreatedNewDemand = { | ||
demandId: newEntity.id | ||
}; | ||
|
||
this.eventGateway.handleEvent({ | ||
type: SupportedEvents.CREATE_NEW_DEMAND, | ||
data: eventData | ||
}); | ||
|
||
return newEntity; | ||
} | ||
|
||
@Delete('/:id') | ||
async delete(@Param('id') id: string) { | ||
const existing = await this.demandRepository.findOne(id); | ||
|
||
if (!existing) { | ||
throw new NotFoundException(StorageErrors.NON_EXISTENT); | ||
} | ||
|
||
existing.status = DemandStatus.ARCHIVED; | ||
|
||
try { | ||
await existing.save(); | ||
|
||
return { | ||
message: `Demand ${id} successfully archived` | ||
}; | ||
} catch (error) { | ||
throw new UnprocessableEntityException({ | ||
message: `Demand ${id} could not be archived due to an unknown error` | ||
}); | ||
} | ||
} | ||
|
||
@Put('/:id') | ||
async put(@Param('id') id: string, @Body() body: DemandUpdateData) { | ||
const existing = await this.demandRepository.findOne(id); | ||
|
||
if (!existing) { | ||
throw new NotFoundException(StorageErrors.NON_EXISTENT); | ||
} | ||
|
||
existing.status = body.status ?? existing.status; | ||
|
||
if (body.demandPartiallyFilledEvent) { | ||
existing.demandPartiallyFilledEvents.push( | ||
JSON.stringify(body.demandPartiallyFilledEvent) | ||
); | ||
} | ||
|
||
const hasNewFillEvent = body.demandPartiallyFilledEvent !== null; | ||
|
||
if (hasNewFillEvent) { | ||
existing.demandPartiallyFilledEvents.push( | ||
JSON.stringify(body.demandPartiallyFilledEvent) | ||
); | ||
} | ||
|
||
try { | ||
await existing.save(); | ||
} catch (error) { | ||
throw new UnprocessableEntityException({ | ||
message: `Demand ${id} could not be updated due to an unkown error` | ||
}); | ||
} | ||
|
||
this.eventGateway.handleEvent({ | ||
type: SupportedEvents.DEMAND_UPDATED, | ||
data: { demandId: existing.id } | ||
}); | ||
|
||
if (hasNewFillEvent) { | ||
const eventData: DemandPartiallyFilledEvent = { | ||
demandId: existing.id, | ||
certificateId: body.demandPartiallyFilledEvent.certificateId, | ||
energy: body.demandPartiallyFilledEvent.energy, | ||
blockNumber: body.demandPartiallyFilledEvent.blockNumber | ||
}; | ||
|
||
this.eventGateway.handleEvent({ | ||
type: SupportedEvents.DEMAND_PARTIALLY_FILLED, | ||
data: eventData | ||
}); | ||
} | ||
|
||
return { | ||
message: `Demand ${id} successfully updated` | ||
}; | ||
} | ||
} |
Oops, something went wrong.