Skip to content

Commit

Permalink
feat: added communications back-end #78
Browse files Browse the repository at this point in the history
  • Loading branch information
rbento1096 committed Feb 17, 2024
1 parent 86ad0af commit 348d5a1
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 1 deletion.
6 changes: 5 additions & 1 deletion back-end/deploy/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const apiResources: ResourceController[] = [
{ name: 'users', paths: ['/users', '/users/{userId}'] },
{ name: 'eventSpots', paths: ['/event-spots', '/event-spots/{spotId}'] },
{ name: 'usefulLinks', paths: ['/useful-links', '/useful-links/{linkId}'] },
{ name: 'venues', paths: ['/venues', '/venues/{venueId}'] }
{ name: 'venues', paths: ['/venues', '/venues/{venueId}'] },
{ name: 'communications', paths: ['/communications', '/communications/{communicationId}'] }
];

const tables: { [tableName: string]: DDBTable } = {
Expand All @@ -46,6 +47,9 @@ const tables: { [tableName: string]: DDBTable } = {
},
venues: {
PK: { name: 'venueId', type: DDB.AttributeType.STRING }
},
communications: {
PK: { name: 'communicationId', type: DDB.AttributeType.STRING }
}
};

Expand Down
159 changes: 159 additions & 0 deletions back-end/src/handlers/communications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
///
/// IMPORTS
///

import { DynamoDB, RCError, ResourceController } from 'idea-aws';

import { Communication, CommunicationWithMarker } from '../models/communication.model';
import { User } from '../models/user.model';

///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
///

const PROJECT = process.env.PROJECT;

const DDB_TABLES = {
users: process.env.DDB_TABLE_users,
communications: process.env.DDB_TABLE_communications,
usersReadCommunications: process.env.DDB_TABLE_usersReadCommunications
};

const ddb = new DynamoDB();

export const handler = (ev: any, _: any, cb: any) => new Communications(ev, cb).handleRequest();

///
/// RESOURCE CONTROLLER
///

// @todo check permissions here

class Communications extends ResourceController {
user: User;
communication: Communication;

constructor(event: any, callback: any) {
super(event, callback, { resourceId: 'communicationId' });
}

protected async checkAuthBeforeRequest(): Promise<void> {
try {
this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
} catch (err) {
throw new RCError('User not found');
}

if (!this.resourceId) return;

try {
this.communication = new Communication(
await ddb.get({ TableName: DDB_TABLES.communications, Key: { communicationId: this.resourceId } })
);
} catch (err) {
throw new RCError('Communication not found');
}
}

protected async getResource(): Promise<CommunicationWithMarker> {
const communication = this.communication as CommunicationWithMarker;

try {
await ddb.get({
TableName: DDB_TABLES.usersReadCommunications,
Key: { userId: this.principalId, communicationId: this.resourceId }
});
communication.hasBeenReadByUser = true;
return communication;
} catch (unread) {
return communication;
}
}

protected async putResource(): Promise<Communication> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

const oldResource = new Communication(this.communication);
this.communication.safeLoad(this.body, oldResource);

return await this.putSafeResource();
}
private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise<Communication> {
const errors = this.communication.validate();
if (errors.length) throw new RCError(`Invalid fields: ${errors.join(', ')}`);

try {
const putParams: any = { TableName: DDB_TABLES.communications, Item: this.communication };
if (opts.noOverwrite) putParams.ConditionExpression = 'attribute_not_exists(communicationId)';
await ddb.put(putParams);

return this.communication;
} catch (err) {
throw new RCError('Operation failed');
}
}

protected async patchResource(): Promise<void> {
switch (this.body.action) {
case 'MARK_AS_READ':
return await this.markAsReadForUser(true);
case 'MARK_AS_UNREAD':
return await this.markAsReadForUser(false);
default:
throw new RCError('Unsupported action');
}
}
private async markAsReadForUser(markRead: boolean): Promise<void> {
const marker = { userId: this.principalId, communicationId: this.resourceId };

if (markRead) await ddb.put({ TableName: DDB_TABLES.usersReadCommunications, Item: marker });
else await ddb.delete({ TableName: DDB_TABLES.usersReadCommunications, Key: marker });
}

protected async deleteResource(): Promise<void> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

try {
await ddb.delete({ TableName: DDB_TABLES.communications, Key: { communicationId: this.resourceId } });
} catch (err) {
throw new RCError('Delete failed');
}
}

protected async postResources(): Promise<Communication> {
if (!this.user.permissions.canManageContents) throw new RCError('Unauthorized');

this.communication = new Communication(this.body);
this.communication.communicationId = await ddb.IUNID(PROJECT);

return await this.putSafeResource({ noOverwrite: true });
}

protected async getResources(): Promise<CommunicationWithMarker[]> {
try {
const communications = (await ddb.scan({ TableName: DDB_TABLES.communications })).map(
x => new CommunicationWithMarker(x)
);

const usersReadCommunications = new Set(
(
await ddb.query({
TableName: DDB_TABLES.usersReadCommunications,
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': this.principalId }
})
).map(x => x.communicationId)
);

communications.forEach(c => {
if (usersReadCommunications.has(c.communicationId)) c.hasBeenReadByUser = true;
});

const sortedCommunications = communications.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));

return sortedCommunications;
} catch (err) {
throw new RCError('Operation failed');
}
}
}
56 changes: 56 additions & 0 deletions back-end/src/models/communication.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { epochISOString, isEmpty, Resource } from 'idea-toolbox';

export class Communication extends Resource {
/**
* The communication ID.
*/
communicationId: string;
/**
* The title of the communication.
*/
title: string;
/**
* The content of the communication.
*/
content: string;
/**
* The date of publishing for the communication.
*/
publishedAt: epochISOString;
/**
* The URI for an image to display on this communication.
*/
imageURI: string;

// @todo do we add more stuff here? Push notifications, links, etc...

load(x: any): void {
super.load(x);
this.communicationId = this.clean(x.communicationId, String);
this.title = this.clean(x.title, String);
this.content = this.clean(x.content, String);
this.publishedAt = this.clean(x.publishedAt, t => new Date(t).toISOString(), new Date().toISOString());
this.imageURI = this.clean(x.imageURI, String);
}
safeLoad(newData: any, safeData: any): void {
super.safeLoad(newData, safeData);
this.communicationId = safeData.communicationId;
}
validate(): string[] {
const e = super.validate();
if (isEmpty(this.title)) e.push('title');
if (isEmpty(this.content)) e.push('content');
if (isEmpty(this.publishedAt, 'date')) e.push('publishedAt');
return e;
}
}

// @todo check if this is needed
export class CommunicationWithMarker extends Communication {
hasBeenReadByUser?: boolean;

load(x: any): void {
super.load(x);
if (x.hasBeenReadByUser) this.hasBeenReadByUser = true;
}
}
129 changes: 129 additions & 0 deletions back-end/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ tags:
description: The useful links available to the users to explore more contents
- name: Venues
description: The venues of the event
- name: Communications
description: The event's communications and announcements.

paths:
/status:
Expand Down Expand Up @@ -562,6 +564,117 @@ paths:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'
/communications:
get:
summary: Get the communications
tags: [Communications]
security:
- AuthFunction: []
responses:
200:
$ref: '#/components/responses/Communications'
post:
summary: Insert a new communication
description: Requires to be content manager
tags: [Communications]
security:
- AuthFunction: []
requestBody:
required: true
description: Communication
content:
application/json:
schema:
type: object
responses:
200:
$ref: '#/components/responses/Communication'
400:
$ref: '#/components/responses/BadParameters'
/communications/{communicationId}:
get:
summary: Get a communication
tags: [Communications]
security:
- AuthFunction: []
parameters:
- name: communicationId
in: path
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/Communication'
put:
summary: Edit a communication
description: Requires to be content manager
tags: [Communications]
security:
- AuthFunction: []
parameters:
- name: communicationId
in: path
required: true
schema:
type: string
requestBody:
required: true
description: Communication
content:
application/json:
schema:
type: object
responses:
200:
$ref: '#/components/responses/Communication'
400:
$ref: '#/components/responses/BadParameters'
patch:
summary: Actions on a communication
tags: [Communications, Users]
security:
- AuthFunction: []
parameters:
- name: communicationId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
action:
type: string
enum:
- MARK_AS_READ
- MARK_AS_UNREAD
responses:
200:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'
delete:
summary: Delete a communication
description: Requires to be content manager
tags: [Communications]
security:
- AuthFunction: []
parameters:
- name: communicationId
in: path
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'

components:
schemas:
Expand Down Expand Up @@ -665,6 +778,22 @@ components:
type: array
items:
$ref: '#/components/schemas/Venue'
Communication:
description: Communication
content:
application/json:
schema:
type: object
items:
$ref: '#/components/schemas/Communication'
Communications:
description: Communication[]
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Communication'
BadParameters:
description: Bad input parameters
content:
Expand Down

0 comments on commit 348d5a1

Please sign in to comment.