Skip to content

Commit

Permalink
feat: Resource, ResourceFeedback and ResourceUser modules + storyblok…
Browse files Browse the repository at this point in the history
… webhook fixes (#712)
  • Loading branch information
eleanorreem authored Dec 3, 2024
1 parent ad8f635 commit a7bfb12
Show file tree
Hide file tree
Showing 43 changed files with 1,074 additions and 216 deletions.
6 changes: 6 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { PartnerAccessModule } from './partner-access/partner-access.module';
import { PartnerAdminModule } from './partner-admin/partner-admin.module';
import { PartnerFeatureModule } from './partner-feature/partner-feature.module';
import { PartnerModule } from './partner/partner.module';
import { ResourceFeedbackModule } from './resource-feedback/resource-feedback.module';
import { ResourceUserModule } from './resource-user/resource-user.module';
import { ResourceModule } from './resource/resource.module';
import { SessionFeedbackModule } from './session-feedback/session-feedback.module';
import { SessionUserModule } from './session-user/session-user.module';
import { SessionModule } from './session/session.module';
Expand Down Expand Up @@ -47,6 +50,9 @@ import { WebhooksModule } from './webhooks/webhooks.module';
HealthModule,
CrispModule,
CrispListenerModule,
ResourceModule,
ResourceUserModule,
ResourceFeedbackModule,
],
})
export class AppModule {}
24 changes: 24 additions & 0 deletions src/entities/resource-feedback.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { FEEDBACK_TAGS_ENUM } from '../utils/constants';
import { BaseBloomEntity } from './base.entity';
import { ResourceEntity } from './resource.entity';

@Entity({ name: 'resource_feedback' })
export class ResourceFeedbackEntity extends BaseBloomEntity {
@PrimaryGeneratedColumn('uuid', { name: 'resourceFeedbackId' })
id: string;

@Column()
resourceId: string;

@ManyToOne(() => ResourceEntity, (resourceEntity) => resourceEntity.resourceFeedback, {
onDelete: 'CASCADE',
})
resource: ResourceEntity;

@Column()
feedbackTags: FEEDBACK_TAGS_ENUM;

@Column()
feedbackDescription: string;
}
23 changes: 23 additions & 0 deletions src/entities/resource-user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { BaseBloomEntity } from './base.entity';
import { ResourceEntity } from './resource.entity';
import { UserEntity } from './user.entity';

// Many to many join table documentation can be found here: https://orkhan.gitbook.io/typeorm/docs/many-to-many-relations#many-to-many-relations-with-custom-properties

@Entity({ name: 'resource_user' })
export class ResourceUserEntity extends BaseBloomEntity {
@PrimaryGeneratedColumn('uuid', { name: 'resourceUserId' })
id: string;

@Column({ type: 'date', nullable: true })
completedAt: Date;

@ManyToOne(() => ResourceEntity, (resourceEntity) => resourceEntity.resourceUser, {
onDelete: 'CASCADE',
})
resource: ResourceEntity;

@ManyToOne(() => UserEntity, (userEntity) => userEntity.resourceUser, { onDelete: 'CASCADE' })
user: UserEntity;
}
53 changes: 53 additions & 0 deletions src/entities/resource.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { RESOURCE_CATEGORIES, STORYBLOK_STORY_STATUS_ENUM } from '../utils/constants';
import { BaseBloomEntity } from './base.entity';
import { ResourceFeedbackEntity } from './resource-feedback.entity';
import { ResourceUserEntity } from './resource-user.entity';

@Entity({ name: 'resource' })
export class ResourceEntity extends BaseBloomEntity {
@PrimaryGeneratedColumn('uuid', { name: 'resourceId' })
id: string;

@Column()
name: string;

@Column()
slug: string;

@Column({
nullable: true,
})
status: STORYBLOK_STORY_STATUS_ENUM;

@Column({
nullable: false,
})
category: RESOURCE_CATEGORIES;

@Column({
unique: true,
nullable: true,
})
storyblokUuid: string;

@Column({
unique: true,
nullable: true,
})
storyblokId: number;

@OneToMany(() => ResourceUserEntity, (resourceUserEntity) => resourceUserEntity.resource, {
cascade: true,
})
resourceUser: ResourceUserEntity[];

@OneToMany(
() => ResourceFeedbackEntity,
(resourceFeedbackEntity) => resourceFeedbackEntity.resource,
{
cascade: true,
},
)
resourceFeedback: ResourceFeedbackEntity[];
}
7 changes: 2 additions & 5 deletions src/entities/session-feedback.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Column, Entity, JoinTable, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { FEEDBACK_TAGS_ENUM } from '../utils/constants';
import { BaseBloomEntity } from './base.entity';
import { SessionEntity } from './session.entity';
Expand All @@ -8,12 +8,9 @@ export class SessionFeedbackEntity extends BaseBloomEntity {
@PrimaryGeneratedColumn('uuid', { name: 'sessionFeedbackId' })
sessionFeedbackId: string;

@Column()
sessionId: string;
@ManyToOne(() => SessionEntity, (sessionEntity) => sessionEntity.sessionUser, {
@ManyToOne(() => SessionEntity, (sessionEntity) => sessionEntity.sessionFeedback, {
onDelete: 'CASCADE',
})
@JoinTable({ name: 'session', joinColumn: { name: 'sessionId' } })
session: SessionEntity;

@Column()
Expand Down
1 change: 0 additions & 1 deletion src/entities/session-user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export class SessionUserEntity extends BaseBloomEntity {
@ManyToOne(() => SessionEntity, (sessionEntity) => sessionEntity.sessionUser, {
onDelete: 'CASCADE',
})
@JoinTable({ name: 'session', joinColumn: { name: 'sessionId' } })
session: SessionEntity;

@Column()
Expand Down
10 changes: 10 additions & 0 deletions src/entities/session.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column, Entity, JoinTable, ManyToOne, OneToMany, PrimaryGeneratedColumn
import { STORYBLOK_STORY_STATUS_ENUM } from '../utils/constants';
import { BaseBloomEntity } from './base.entity';
import { CourseEntity } from './course.entity';
import { SessionFeedbackEntity } from './session-feedback.entity';
import { SessionUserEntity } from './session-user.entity';

@Entity({ name: 'session' })
Expand Down Expand Up @@ -42,4 +43,13 @@ export class SessionEntity extends BaseBloomEntity {
cascade: true,
})
sessionUser: SessionUserEntity[];

@OneToMany(
() => SessionFeedbackEntity,
(sessionFeedbackEntity) => sessionFeedbackEntity.session,
{
cascade: true,
},
)
sessionFeedback: SessionFeedbackEntity[];
}
6 changes: 6 additions & 0 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EMAIL_REMINDERS_FREQUENCY } from '../utils/constants';
import { BaseBloomEntity } from './base.entity';
import { CourseUserEntity } from './course-user.entity';
import { EventLogEntity } from './event-log.entity';
import { ResourceUserEntity } from './resource-user.entity';
import { SubscriptionUserEntity } from './subscription-user.entity';
import { TherapySessionEntity } from './therapy-session.entity';

Expand Down Expand Up @@ -55,6 +56,11 @@ export class UserEntity extends BaseBloomEntity {
@OneToMany(() => CourseUserEntity, (courseUser) => courseUser.user, { cascade: true })
courseUser: CourseUserEntity[];

@OneToMany(() => ResourceUserEntity, (resourceUser) => resourceUser.user, {
cascade: true,
})
resourceUser: ResourceUserEntity[];

@OneToMany(() => SubscriptionUserEntity, (subscriptionUser) => subscriptionUser.user, {
cascade: true,
})
Expand Down
4 changes: 2 additions & 2 deletions src/event-logger/event-logger.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ describe('EventLoggerService', () => {
expect(service).toBeDefined();
});
describe('createEventLog', () => {
it('when supplied with correct data should return new feature', async () => {
it('should create and return an event log record', async () => {
const response = await service.createEventLog({
event: EVENT_NAME.CHAT_MESSAGE_SENT,
date: new Date(2000, 1, 1),
userId: 'userId',
});
expect(response).toMatchObject({
id: 'newId',
id: 'logId',
event: EVENT_NAME.CHAT_MESSAGE_SENT,
date: new Date(2000, 1, 1),
userId: 'userId',
Expand Down
8 changes: 1 addition & 7 deletions src/event-logger/event-logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,7 @@ export class EventLoggerService {
userId = user.id;
}

const eventLog = await this.eventLoggerRepository.create({
userId,
event,
date,
});
const eventLogRecord = this.eventLoggerRepository.save(eventLog);
return eventLogRecord;
return await this.eventLoggerRepository.save({ userId, event, date });
} catch (err) {
throw new HttpException(
`createEventLog - failed to create event log ${err}`,
Expand Down
57 changes: 57 additions & 0 deletions src/migrations/1733160378757-bloom-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class BloomBackend1733160378757 implements MigrationInterface {
name = 'BloomBackend1733160378757';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "resource_feedback" ("createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "resourceFeedbackId" uuid NOT NULL DEFAULT uuid_generate_v4(), "resourceId" uuid NOT NULL, "feedbackTags" character varying NOT NULL, "feedbackDescription" character varying NOT NULL, CONSTRAINT "PK_97393ce3b5c5d462500e181613b" PRIMARY KEY ("resourceFeedbackId"))`,
);
await queryRunner.query(
`CREATE TABLE "resource" ("createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "resourceId" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "slug" character varying NOT NULL, "status" character varying, "category" character varying NOT NULL, "storyblokUuid" character varying, "storyblokId" integer, CONSTRAINT "UQ_575686fbc2bc272030a15ac3ea1" UNIQUE ("storyblokUuid"), CONSTRAINT "UQ_1b4b84228b725114ccc955dcec7" UNIQUE ("storyblokId"), CONSTRAINT "PK_f59f8360a61e63c72d0f1a6aa00" PRIMARY KEY ("resourceId"))`,
);
await queryRunner.query(
`CREATE TABLE "resource_user" ("createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "resourceUserId" uuid NOT NULL DEFAULT uuid_generate_v4(), "completedAt" date, "resourceId" uuid, "userId" uuid, CONSTRAINT "PK_e88a671cf058fac35384d8e1426" PRIMARY KEY ("resourceUserId"))`,
);
await queryRunner.query(
`ALTER TABLE "session_feedback" DROP CONSTRAINT "FK_a0567dbf6bd30cf4bd05b110a17"`,
);
await queryRunner.query(
`ALTER TABLE "session_feedback" ALTER COLUMN "sessionId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "session_feedback" ADD CONSTRAINT "FK_a0567dbf6bd30cf4bd05b110a17" FOREIGN KEY ("sessionId") REFERENCES "session"("sessionId") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "resource_feedback" ADD CONSTRAINT "FK_3218ac4ae760f580ce260a43e3a" FOREIGN KEY ("resourceId") REFERENCES "resource"("resourceId") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "resource_user" ADD CONSTRAINT "FK_774b61a463074cee88e57685925" FOREIGN KEY ("resourceId") REFERENCES "resource"("resourceId") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "resource_user" ADD CONSTRAINT "FK_ea89e3c7f0126d7e9d02308c2ca" FOREIGN KEY ("userId") REFERENCES "user"("userId") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "resource_user" DROP CONSTRAINT "FK_ea89e3c7f0126d7e9d02308c2ca"`,
);
await queryRunner.query(
`ALTER TABLE "resource_user" DROP CONSTRAINT "FK_774b61a463074cee88e57685925"`,
);
await queryRunner.query(
`ALTER TABLE "resource_feedback" DROP CONSTRAINT "FK_3218ac4ae760f580ce260a43e3a"`,
);
await queryRunner.query(
`ALTER TABLE "session_feedback" DROP CONSTRAINT "FK_a0567dbf6bd30cf4bd05b110a17"`,
);
await queryRunner.query(`ALTER TABLE "session_feedback" ALTER COLUMN "sessionId" SET NOT NULL`);
await queryRunner.query(
`ALTER TABLE "session_feedback" ADD CONSTRAINT "FK_a0567dbf6bd30cf4bd05b110a17" FOREIGN KEY ("sessionId") REFERENCES "session"("sessionId") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(`DROP TABLE "resource_user"`);
await queryRunner.query(`DROP TABLE "resource"`);
await queryRunner.query(`DROP TABLE "resource_feedback"`);
}
}
1 change: 1 addition & 0 deletions src/partner-admin/partner-admin-auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const userEntity: UserEntity = {
signUpLanguage: 'en',
subscriptionUser: [],
therapySession: [],
resourceUser: [],
eventLog: [],
};
describe('PartnerAdminAuthGuard', () => {
Expand Down
23 changes: 23 additions & 0 deletions src/resource-feedback/dtos/create-resource-feedback.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { FEEDBACK_TAGS_ENUM } from 'src/utils/constants';

export class CreateResourceFeedbackDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ type: String })
resourceId: string;

@IsNotEmpty()
@IsEnum(FEEDBACK_TAGS_ENUM)
@ApiProperty({
enum: FEEDBACK_TAGS_ENUM,
type: String,
example: Object.values(FEEDBACK_TAGS_ENUM),
})
feedbackTags: FEEDBACK_TAGS_ENUM;

@IsString()
@ApiProperty({ type: String })
feedbackDescription: string;
}
28 changes: 28 additions & 0 deletions src/resource-feedback/dtos/resource-feedback.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { FEEDBACK_TAGS_ENUM } from 'src/utils/constants';

export class ResourceFeedbackDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ type: String })
id: string;

@IsNotEmpty()
@IsString()
@ApiProperty({ type: String })
resourceId: string;

@IsNotEmpty()
@IsEnum(FEEDBACK_TAGS_ENUM)
@ApiProperty({
enum: FEEDBACK_TAGS_ENUM,
type: String,
example: Object.values(FEEDBACK_TAGS_ENUM),
})
feedbackTags: FEEDBACK_TAGS_ENUM;

@IsString()
@ApiProperty({ type: String })
feedbackDescription: string;
}
21 changes: 21 additions & 0 deletions src/resource-feedback/resource-feedback.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ControllerDecorator } from 'src/utils/controller.decorator';
import { CreateResourceFeedbackDto } from './dtos/create-resource-feedback.dto';
import { ResourceFeedbackService } from './resource-feedback.service';

@ApiTags('Resource Feedback')
@ControllerDecorator()
@Controller('/v1/resource-feedback')
export class ResourceFeedbackController {
constructor(private readonly resourceFeedbackService: ResourceFeedbackService) {}

// TODO how do we protect this public endpoint from being abused?
@Post()
@ApiOperation({
description: 'Stores feedback from a user',
})
create(@Body() createResourceFeedbackDto: CreateResourceFeedbackDto) {
return this.resourceFeedbackService.create(createResourceFeedbackDto);
}
}
14 changes: 14 additions & 0 deletions src/resource-feedback/resource-feedback.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ResourceFeedbackEntity } from 'src/entities/resource-feedback.entity';
import { ResourceEntity } from 'src/entities/resource.entity';
import { ResourceService } from 'src/resource/resource.service';
import { ResourceFeedbackController } from './resource-feedback.controller';
import { ResourceFeedbackService } from './resource-feedback.service';

@Module({
imports: [TypeOrmModule.forFeature([ResourceFeedbackEntity, ResourceEntity])],
controllers: [ResourceFeedbackController],
providers: [ResourceFeedbackService, ResourceService],
})
export class ResourceFeedbackModule {}
Loading

0 comments on commit a7bfb12

Please sign in to comment.