diff --git a/.forestadmin-schema.json b/.forestadmin-schema.json index c10fb49f5..c2a1d900b 100644 --- a/.forestadmin-schema.json +++ b/.forestadmin-schema.json @@ -1793,6 +1793,40 @@ "type": ["String"], "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "QuestionAndAnswers_through_Member_createdBy", + "integration": null, + "inverseOf": "Member_through_createdBy", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "QuestionAndAnswer.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "QuestionAndAnswers_through_Member_modifiedBy", + "integration": null, + "inverseOf": "Member_through_modifiedBy", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "QuestionAndAnswer.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -3105,6 +3139,23 @@ "type": ["String"], "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "QuestionAndAnswers", + "integration": null, + "inverseOf": "PLEvent", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "QuestionAndAnswer.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -3922,6 +3973,23 @@ "type": ["String"], "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "QuestionAndAnswers", + "integration": null, + "inverseOf": "Project", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "QuestionAndAnswer.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -4474,6 +4542,323 @@ "paginationType": "page", "segments": [] }, + { + "actions": [], + "fields": [ + { + "defaultValue": null, + "enums": null, + "field": "Member_through_createdBy", + "integration": null, + "inverseOf": "QuestionAndAnswers_through_Member_createdBy", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "Member.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "Member_through_modifiedBy", + "integration": null, + "inverseOf": "QuestionAndAnswers_through_Member_modifiedBy", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "Member.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "PLEvent", + "integration": null, + "inverseOf": "QuestionAndAnswers", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "PLEvent.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "Project", + "integration": null, + "inverseOf": "QuestionAndAnswers", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "Project.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "Team", + "integration": null, + "inverseOf": "QuestionAndAnswers", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": "Team.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "answer", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "answerSourceFrom", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "answerSources", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": ["Json"], + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "content", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "createdAt", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Date", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "id", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": true, + "isReadOnly": true, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [] + }, + { + "defaultValue": true, + "enums": null, + "field": "isActive", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Boolean", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "relatedQuestions", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": ["Json"], + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "shareCount", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "slug", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "uid", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "updatedAt", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Date", + "validations": [] + }, + { + "defaultValue": null, + "enums": null, + "field": "viewCount", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Number", + "validations": [] + } + ], + "icon": null, + "integration": null, + "isReadOnly": false, + "isSearchable": true, + "isVirtual": false, + "name": "QuestionAndAnswer", + "onlyForRelationships": false, + "paginationType": "page", + "segments": [] + }, { "actions": [], "fields": [ @@ -4706,6 +5091,23 @@ "type": ["String"], "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "QuestionAndAnswers", + "integration": null, + "inverseOf": "Team", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "QuestionAndAnswer.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, { "defaultValue": null, "enums": null, diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index 7506d2ad4..4aefeccaa 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -48,6 +48,7 @@ model Team { eventGuests PLEventGuest[] teamFocusAreas TeamFocusArea[] teamFocusAreasVersionHistory TeamFocusAreaVersionHistory[] + relatedQuestions QuestionAndAnswer[] @relation("TeamRelatedQuestionAndAnswers") } model Member { @@ -88,6 +89,8 @@ model Member { targetInteractions MemberInteraction[] @relation("TargetMemberInteractions") followUps MemberFollowUp[] feedbacks MemberFeedback[] + createdQuestions QuestionAndAnswer[] @relation("MemberCreatedQuestionAndAnswers") + modifiedQuestions QuestionAndAnswer[] @relation("MemberModifiedQuestionAndAnswers") } model MemberRole { @@ -335,33 +338,35 @@ model Project { isFeatured Boolean? @default(false) projectFocusAreas ProjectFocusArea[] contributions ProjectContribution[] + relatedQuestions QuestionAndAnswer[] @relation("ProjectRelatedQuestionAndAnswers") } model PLEvent { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) type PLEventType? eventsCount Int? telegramId String? logoUid String? - logo Image? @relation("logo", fields: [logoUid], references: [uid]) + logo Image? @relation("logo", fields: [logoUid], references: [uid]) bannerUid String? - banner Image? @relation("banner", fields: [bannerUid], references: [uid]) + banner Image? @relation("banner", fields: [bannerUid], references: [uid]) name String description String? shortDescription String? websiteURL String? - isFeatured Boolean? @default(false) + isFeatured Boolean? @default(false) location String - slugURL String @unique + slugURL String @unique resources Json[] priority Int? additionalInfo Json? startDate DateTime endDate DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt eventGuests PLEventGuest[] + relatedQuestions QuestionAndAnswer[] @relation("PLEventRelatedQuestionAndAnswers") } model PLEventGuest { @@ -500,4 +505,29 @@ model MemberFeedback { updatedAt DateTime @updatedAt } - +model QuestionAndAnswer { + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) + title String? + content String + viewCount Int? + shareCount Int? + slug String @unique + isActive Boolean @default(true) + teamUid String? + team Team? @relation("TeamRelatedQuestionAndAnswers", fields: [teamUid], references: [uid]) + projectUid String? + project Project? @relation("ProjectRelatedQuestionAndAnswers", fields: [projectUid], references: [uid]) + eventUid String? + plevent PLEvent? @relation("PLEventRelatedQuestionAndAnswers", fields: [eventUid], references: [uid]) + createdBy String + creator Member? @relation("MemberCreatedQuestionAndAnswers", fields: [createdBy], references: [uid]) + modifiedBy String + modifier Member? @relation("MemberModifiedQuestionAndAnswers", fields: [modifiedBy], references: [uid]) + answer String? + answerSources Json[] + answerSourceFrom String? + relatedQuestions Json[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/apps/web-api/src/focus-areas/focus-areas.service.ts b/apps/web-api/src/focus-areas/focus-areas.service.ts index 0337f0732..ae3feb432 100644 --- a/apps/web-api/src/focus-areas/focus-areas.service.ts +++ b/apps/web-api/src/focus-areas/focus-areas.service.ts @@ -50,6 +50,15 @@ export class FocusAreasService { ...this.buildTeamFilter(query) } }, + select: { + team: { + select: { + uid: true, + name: true, + logo: true + } + } + }, distinct: "teamUid" } } @@ -62,6 +71,15 @@ export class FocusAreasService { ...this.buildProjectFilter(query) } }, + select: { + project: { + select: { + uid: true, + name: true, + logo: true + } + } + }, distinct: "projectUid" } } diff --git a/apps/web-api/src/home/home.controller.ts b/apps/web-api/src/home/home.controller.ts index f0f4662e5..0482a2b61 100644 --- a/apps/web-api/src/home/home.controller.ts +++ b/apps/web-api/src/home/home.controller.ts @@ -1,18 +1,106 @@ -import { Controller, Req } from '@nestjs/common'; +import { Controller, Req, Body, Param, UsePipes, UseGuards, ForbiddenException, ConflictException } from '@nestjs/common'; import { Api, initNestServer } from '@ts-rest/nest'; +import { ZodValidationPipe } from 'nestjs-zod'; import { Request } from 'express'; +import { ApiQueryFromZod } from '../decorators/api-query-from-zod'; +import { ApiOkResponseFromZod } from '../decorators/api-response-from-zod'; import { apiHome } from 'libs/contracts/src/lib/contract-home'; import { HomeService } from './home.service'; +import { + QuestionAndAnswerQueryParams, + ResponseQuestionAndAnswerSchemaWithRelations, + ResponseQuestionAndAnswerSchema, + CreateQuestionAndAnswerSchemaDto, + UpdateQuestionAndAnswerSchemaDto +} from 'libs/contracts/src/schema'; +import { UserTokenValidation } from '../guards/user-token-validation.guard'; +import { MembersService } from '../members/members.service'; +import { NoCache } from '../decorators/no-cache.decorator'; +import { PrismaQueryBuilder } from '../utils/prisma-query-builder'; +import { prismaQueryableFieldsFromZod } from '../utils/prisma-queryable-fields-from-zod'; const server = initNestServer(apiHome); type RouteShape = typeof server.routeShapes; @Controller() export class HomeController { - constructor(private homeService: HomeService) {} + constructor( + private homeService: HomeService, + private memberService: MembersService + ) {} @Api(server.route.getAllFeaturedData) - async getAllFeaturedData(@Req() request: Request) { + async getAllFeaturedData() { return await this.homeService.fetchAllFeaturedData(); } + + @Api(server.route.getAllQuestionAndAnswers) + @ApiQueryFromZod(QuestionAndAnswerQueryParams) + @ApiOkResponseFromZod(ResponseQuestionAndAnswerSchemaWithRelations.array()) + @NoCache() + async getQuestionAndAnswers(@Req() request: Request) { + const queryableFields = prismaQueryableFieldsFromZod( + ResponseQuestionAndAnswerSchema + ); + const builder = new PrismaQueryBuilder(queryableFields); + const builtQuery = builder.build(request.query); + return await this.homeService.fetchQuestionAndAnswers(builtQuery); + } + + + @Api(server.route.getQuestionAndAnswer) + @ApiQueryFromZod(QuestionAndAnswerQueryParams) + @ApiOkResponseFromZod(ResponseQuestionAndAnswerSchemaWithRelations) + @NoCache() + async getQuestionAndAnswer(@Param('slug') slug: string) + { + return await this.homeService.fetchQuestionAndAnswerBySlug(slug); + } + + @Api(server.route.createQuestionAndAnswer) + @UsePipes(ZodValidationPipe) + @UseGuards(UserTokenValidation) + async addQuestionAndAnswer( + @Body() questionAndAnswer: CreateQuestionAndAnswerSchemaDto, + @Req() request + ) { + const userEmail = request["userEmail"]; + const member: any = await this.memberService.findMemberByEmail(userEmail); + const result = await this.memberService.checkIfAdminUser(member); + if (!result) { + throw new ForbiddenException(`Member with email ${userEmail} isn't admin`); + } + return await this.homeService.createQuestionAndAnswer(questionAndAnswer as any, member); + } + + @Api(server.route.updateQuestionAndAnswer) + @UsePipes(ZodValidationPipe) + @UseGuards(UserTokenValidation) + async modifyQuestionAndAnswer( + @Param('slug') slug: string, + @Body() questionAndAnswer: UpdateQuestionAndAnswerSchemaDto, + @Req() request + ) { + const userEmail = request["userEmail"]; + const member: any = await this.memberService.findMemberByEmail(userEmail); + const result = await this.memberService.checkIfAdminUser(member); + if (!result) { + throw new ForbiddenException(`Member with email ${userEmail} isn't admin`); + } + return await this.homeService.updateQuestionAndAnswerBySlug(slug, questionAndAnswer as any, member); + } + + @Api(server.route.updateQuestionAndAnswerViewCount) + async modifyQuestionAndAnswerViewCount( + @Param('slug') slug: string + ) { + return await this.homeService.updateQuestionAndAnswerViewCount(slug); + } + + @Api(server.route.updateQuestionAndAnswerShareCount) + async modifyQuestionAndAnswerShareCount( + @Param('slug') slug: string + ) { + return await this.homeService.updateQuestionAndAnswerShareCount(slug); + } } diff --git a/apps/web-api/src/home/home.service.ts b/apps/web-api/src/home/home.service.ts index 5df1e5672..a4f9c906d 100644 --- a/apps/web-api/src/home/home.service.ts +++ b/apps/web-api/src/home/home.service.ts @@ -1,9 +1,17 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + InternalServerErrorException, + ConflictException, + BadRequestException, + NotFoundException +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import { LogService } from '../shared/log.service'; import { MembersService } from '../members/members.service'; import { TeamsService } from '../teams/teams.service'; import { PLEventsService } from '../pl-events/pl-events.service'; import { ProjectsService } from '../projects/projects.service'; +import { PrismaService } from '../shared/prisma.service'; @Injectable() export class HomeService { @@ -12,17 +20,38 @@ export class HomeService { private memberService: MembersService, private teamsService: TeamsService, private plEventsService: PLEventsService, - private projectsService: ProjectsService + private projectsService: ProjectsService, + private prisma: PrismaService ) {} async fetchAllFeaturedData() { try { - const filter = { where : { isFeatured: true }} return { - members: await this.memberService.findAll(filter), - teams: await this.teamsService.findAll(filter), - events: await this.plEventsService.getPLEvents(filter), - projects: await this.projectsService.getProjects(filter) + members: await this.memberService.findAll({ + where: { isFeatured: true }, + include: { + image: true, + location: true, + skills: true, + teamMemberRoles: { + include: { + team: { + include: { + logo: true, + }, + }, + }, + }, + } + }), + teams: await this.teamsService.findAll( { + where: { isFeatured: true }, + include: { + logo: true, + } + }), + events: await this.plEventsService.getPLEvents({ where : { isFeatured: true }}), + projects: await this.projectsService.getProjects({ where : { isFeatured: true }}) }; } catch (error) { @@ -30,4 +59,153 @@ export class HomeService { throw new InternalServerErrorException('Failed to fetch featured data'); } } -} \ No newline at end of file + + async fetchQuestionAndAnswers( + query: Prisma.QuestionAndAnswerFindManyArgs + ) { + try { + query.include = { + ...query.include, + team: { + select: { + uid: true, + logo: true, + name: true + } + }, + project: { + select: { + uid: true, + logo: true, + name: true + } + }, + plevent: { + select: { + uid: true, + name: true, + logo: true + } + } + } + return await this.prisma.questionAndAnswer.findMany(query); + } + catch (error) { + this.handleErrors(error); + } + } + + async fetchQuestionAndAnswerBySlug( + slug: string + ) { + try { + return await this.prisma.questionAndAnswer.findUnique({ + where: { slug } + }); + } + catch (error) { + this.handleErrors(error); + } + } + + async createQuestionAndAnswer( + questionAndAnswer: Prisma.QuestionAndAnswerUncheckedCreateInput, + loggedInMember + ) { + try { + await this.prisma.questionAndAnswer.create({ + data: { + ... { + createdBy: loggedInMember.uid, + modifiedBy: loggedInMember.uid, + slug: Math.random().toString(36).substring(2, 8) + }, + ...questionAndAnswer + } + }); + return { + msg: "success" + } + } + catch (error) { + this.handleErrors(error); + } + } + + async updateQuestionAndAnswerBySlug( + slug: string, + questionAndAnswer: Prisma.QuestionAndAnswerUncheckedUpdateInput, + loggedInMember + ) { + try { + await this.prisma.questionAndAnswer.update({ + where: { slug }, + data: { + ... { + modifiedBy: loggedInMember.uid, + }, + ...questionAndAnswer + } + }); + return { + msg: `success` + }; + } + catch (error) { + this.handleErrors(error); + } + } + + async updateQuestionAndAnswerViewCount(slug: string) { + try { + await this.prisma.questionAndAnswer.update({ + where: { slug }, + data: { + viewCount: { increment: 1 } + } + }); + return { + msg: `success` + }; + } + catch (error) { + this.handleErrors(error); + } + } + + async updateQuestionAndAnswerShareCount(slug: string) { + try { + await this.prisma.questionAndAnswer.update({ + where: { slug }, + data: { + shareCount: { increment: 1 } + } + }); + return { + msg: `success` + }; + } + catch (error) { + this.handleErrors(error); + } + } + + private handleErrors(error, message?) { + this.logger.error(error); + if (error instanceof Prisma.PrismaClientKnownRequestError) { + switch (error?.code) { + case 'P2002': + throw new ConflictException('Unique key constraint error on Question & Answer:', error.message); + case 'P2003': + throw new BadRequestException('Foreign key constraint error on Question & Answer', error.message); + case 'P2025': + throw new NotFoundException('Question and Answer is not found with slug:' + message); + default: + throw error; + } + } else if (error instanceof Prisma.PrismaClientValidationError) { + throw new BadRequestException('Database field validation error on Question & Answer', error.message); + } + throw error; + }; +} diff --git a/libs/contracts/src/lib/contract-home.ts b/libs/contracts/src/lib/contract-home.ts index 754706b09..724e8547e 100644 --- a/libs/contracts/src/lib/contract-home.ts +++ b/libs/contracts/src/lib/contract-home.ts @@ -1,6 +1,9 @@ import { initContract } from '@ts-rest/core'; import { getAPIVersionAsPath } from '../utils/versioned-path'; - +import { + QuestionAndAnswerQueryParams, + ResponseQuestionAndAnswerSchemaWithRelations +} from '../schema'; const contract = initContract(); export const apiHome = contract.router({ @@ -11,6 +14,60 @@ export const apiHome = contract.router({ responses: { 200: contract.response() }, - summary: 'Get all featured members, projects, teams and events', + summary: 'Get all featured members, projects, teams and events' + }, + getAllQuestionAndAnswers: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/home/question-answers`, + query: QuestionAndAnswerQueryParams, + responses: { + 200: ResponseQuestionAndAnswerSchemaWithRelations.array() + }, + summary: 'Get all the question & answers', + }, + getQuestionAndAnswer: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/home/question-answers/:slug`, + query: QuestionAndAnswerQueryParams, + responses: { + 200: ResponseQuestionAndAnswerSchemaWithRelations.array() + }, + summary: 'Get question & answers', + }, + createQuestionAndAnswer: { + method: 'POST', + path: `${getAPIVersionAsPath('1')}/home/question-answers`, + body: contract.body(), + responses: { + 200: contract.response() + }, + summary: 'Create a new question & answer', + }, + updateQuestionAndAnswer: { + method: 'PUT', + path: `${getAPIVersionAsPath('1')}/home/question-answers/:slug`, + body: contract.body(), + responses: { + 200: contract.response() + }, + summary: 'Update a question & answer by slug' + }, + updateQuestionAndAnswerViewCount: { + method: 'PATCH', + path: `${getAPIVersionAsPath('1')}/home/question-answers/:slug/view-count`, + body: contract.body(), + responses: { + 200: contract.response() + }, + summary: 'Update view count of a question & answer by slug' + }, + updateQuestionAndAnswerShareCount: { + method: 'PATCH', + path: `${getAPIVersionAsPath('1')}/home/question-answers/:slug/share-count`, + body: contract.body(), + responses: { + 200: contract.response() + }, + summary: 'Update share count of a question & answer by slug' } -}); \ No newline at end of file +}); diff --git a/libs/contracts/src/schema/index.ts b/libs/contracts/src/schema/index.ts index 8ec41610d..61dde3b9e 100644 --- a/libs/contracts/src/schema/index.ts +++ b/libs/contracts/src/schema/index.ts @@ -21,4 +21,5 @@ export * from './team-focus-areas'; export * from './project-focus-areas'; export * from './member-interaction'; export * from './member-follow-up'; -export * from './member-feedback'; \ No newline at end of file +export * from './member-feedback'; +export * from './question-answer'; \ No newline at end of file diff --git a/libs/contracts/src/schema/question-answer.ts b/libs/contracts/src/schema/question-answer.ts new file mode 100644 index 000000000..b2689e5fd --- /dev/null +++ b/libs/contracts/src/schema/question-answer.ts @@ -0,0 +1,70 @@ +import { createZodDto } from '@abitia/zod-dto'; +import { z } from 'zod'; +import { QueryParams, RETRIEVAL_QUERY_FILTERS } from './query-params'; +import { ResponseMemberWithRelationsSchema } from './member'; +import { ResponseTeamWithRelationsSchema } from './team'; +import { ResponsePLEventSchemaWithRelationsSchema } from './pl-event'; + + +export const QuestionAndAnswerSchema = z.object({ + id: z.number().int(), + uid: z.string(), + title: z.string().optional(), + content: z.string(), + viewCount: z.number().int(), + shareCount: z.number().int(), + isActive: z.boolean().default(true), + teamUid: z.string().nullish(), + eventUid: z.string().nullish(), + projectUid: z.string().nullish(), + answer: z.string().optional(), + answerSources: z.any().optional(), + relatedQuestions: z.any().optional(), + answerSourceFrom: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string() +}); + +export const CreateQuestionAndAnswerSchema = QuestionAndAnswerSchema.pick({ + title: true, + content: true, + viewCount: true, + shareCount: true, + isActive: true, + teamUid: true, + eventUid: true, + projectUid: true, + answer: true, + relatedQuestions: true, + answerSources: true, + answerSourceFrom:true +}); + +export const ResponseQuestionAndAnswerSchema = QuestionAndAnswerSchema.omit({ id: true }).strict(); + +export const ResponseQuestionAndAnswerSchemaWithRelations = ResponseQuestionAndAnswerSchema.extend({ + team: ResponseTeamWithRelationsSchema.optional(), + plevent: ResponsePLEventSchemaWithRelationsSchema.optional(), + creator: ResponseMemberWithRelationsSchema.optional(), + modifier: ResponseMemberWithRelationsSchema.optional() +}); + +export const QuestionAndAnswerRelationalFields = ResponseQuestionAndAnswerSchemaWithRelations.pick({ + team: true, + creator: true, + modifier: true +}).strip(); + +export const QuestionAndAnswerQueryableFields = ResponseQuestionAndAnswerSchema.keyof(); + +export const QuestionAndAnswerQueryParams = QueryParams({ + queryableFields: QuestionAndAnswerQueryableFields, + relationalFields: QuestionAndAnswerRelationalFields +}); + +export const QuestionAndAnswerDetailQueryParams = QuestionAndAnswerQueryParams.unwrap() + .pick(RETRIEVAL_QUERY_FILTERS) + .optional(); + +export class CreateQuestionAndAnswerSchemaDto extends createZodDto(CreateQuestionAndAnswerSchema) {} +export class UpdateQuestionAndAnswerSchemaDto extends createZodDto(CreateQuestionAndAnswerSchema) {} \ No newline at end of file