From a095839c55e90711c708dba7f972456208b9aa8a Mon Sep 17 00:00:00 2001 From: Behzad Rabiei Date: Tue, 24 Sep 2024 17:24:10 +0400 Subject: [PATCH] feat: add discourse table APIs --- src/controllers/discourse.controller.ts | 101 +++++- src/docs/discourse.doc.yml | 325 ++++++++++++++++++ src/docs/platform.doc.yml | 2 + src/routes/v1/discourse.route.ts | 19 + src/services/discourse/core.service.ts | 32 +- src/services/discourse/index.ts | 2 + .../discourse/memberActivity.service.ts | 126 ++++++- src/services/discourse/members.service.ts | 157 +++++++++ src/services/platform.service.ts | 7 +- src/validations/discourse.validation.ts | 77 +++++ src/validations/platform.validation.ts | 1 + 11 files changed, 832 insertions(+), 17 deletions(-) create mode 100644 src/services/discourse/members.service.ts diff --git a/src/controllers/discourse.controller.ts b/src/controllers/discourse.controller.ts index 825ef8e..1bcb859 100644 --- a/src/controllers/discourse.controller.ts +++ b/src/controllers/discourse.controller.ts @@ -5,12 +5,8 @@ import { catchAsync, date, charts } from '../utils'; import { DatabaseManager, Platform } from '@togethercrew.dev/db'; import moment from 'moment-timezone'; import { SupportedNeo4jPlatforms } from '../types/neo4j.type'; -import passport, { use } from 'passport'; -import httpStatus from 'http-status'; -import { ApiError, roleUtil, pick } from '../utils'; -import { communityService, platformService, moduleService } from '../services'; -import { Types } from 'mongoose'; -import { UserRole } from '../interfaces'; +import { pick } from '../utils'; +import { activityCompostionsTypes } from '../config/memberBreakDownTables'; const heatmapChart = catchAsync(async function (req: IAuthAndPlatform, res: Response) { const platformConnection = await DatabaseManager.getInstance().getPlatformDb(req.platform?.id); @@ -43,8 +39,101 @@ const membersInteractionsNetworkGraph = catchAsync(async function (req: IAuthAnd res.send(networkGraphData); }); +const activeMembersCompositionTable = catchAsync(async function (req: IAuthAndPlatform, res: Response) { + const filter = pick({ ...req.query, ...req.body }, ['activityComposition', 'ngu']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const platformConnection = await DatabaseManager.getInstance().getPlatformDb(req.platform?.id); + const activityCompostionFields = + discourseService.memberActivityService.getActivityCompositionOfActiveMembersComposition(); + const memberActivity = await discourseService.memberActivityService.getLastDocumentForTablesUsage( + platformConnection, + activityCompostionFields, + ); + const members = await discourseService.membersService.queryMembersForTables( + platformConnection, + filter, + options, + memberActivity, + activityCompostionsTypes.activeMembersComposition, + ); + if (members) { + members.results.forEach((member) => { + member.ngu = discourseService.membersService.getNgu(member); + member.activityComposition = discourseService.memberActivityService.getActivityComposition( + member, + memberActivity, + filter.activityComposition, + ); + }); + } + res.send(members); +}); + +const activeMembersOnboardingTable = catchAsync(async function (req: IAuthAndPlatform, res: Response) { + const filter = pick({ ...req.query, ...req.body }, ['activityComposition', 'ngu']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const platformConnection = await DatabaseManager.getInstance().getPlatformDb(req.platform?.id); + const activityCompostionFields = + discourseService.memberActivityService.getActivityCompositionOfActiveMembersOnboarding(); + const memberActivity = await discourseService.memberActivityService.getLastDocumentForTablesUsage( + platformConnection, + activityCompostionFields, + ); + const members = await discourseService.membersService.queryMembersForTables( + platformConnection, + filter, + options, + memberActivity, + activityCompostionsTypes.activeMembersOnboarding, + ); + if (members) { + members.results.forEach((member) => { + member.ngu = discourseService.membersService.getNgu(member); + member.activityComposition = discourseService.memberActivityService.getActivityComposition( + member, + memberActivity, + filter.activityComposition, + ); + }); + } + res.send(members); +}); + +const disengagedMembersCompositionTable = catchAsync(async function (req: IAuthAndPlatform, res: Response) { + const filter = pick({ ...req.query, ...req.body }, ['activityComposition', 'ngu']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); + const platformConnection = await DatabaseManager.getInstance().getPlatformDb(req.platform?.id); + const activityCompostionFields = + discourseService.memberActivityService.getActivityCompositionOfDisengagedComposition(); + const memberActivity = await discourseService.memberActivityService.getLastDocumentForTablesUsage( + platformConnection, + activityCompostionFields, + ); + const members = await discourseService.membersService.queryMembersForTables( + platformConnection, + filter, + options, + memberActivity, + activityCompostionsTypes.disengagedMembersCompostion, + ); + if (members) { + members.results.forEach((member) => { + member.ngu = discourseService.membersService.getNgu(member); + member.activityComposition = discourseService.memberActivityService.getActivityComposition( + member, + memberActivity, + filter.activityComposition, + ); + }); + } + res.send(members); +}); + export default { heatmapChart, lineGraph, membersInteractionsNetworkGraph, + activeMembersCompositionTable, + activeMembersOnboardingTable, + disengagedMembersCompositionTable, }; diff --git a/src/docs/discourse.doc.yml b/src/docs/discourse.doc.yml index 63a103f..555b11f 100644 --- a/src/docs/discourse.doc.yml +++ b/src/docs/discourse.doc.yml @@ -241,3 +241,328 @@ paths: '404': description: NotFound $ref: '#/components/responses/NotFound' + /api/v1/discourse/member-activity/{platformId}/active-members-composition-table: + post: + tags: + - [Discourse] + summary: Get data for active members composition table + security: + - bearerAuth: [] + description: for now sortBy just can apply for ngu and joined_at(DaoMemberSince in UI) + parameters: + - in: path + name: platformId + required: true + schema: + type: string + description: Platform Id + - in: query + name: activityComposition + schema: + type: array + items: + type: string + enum: ['all_active', 'all_new_active', 'all_consistent', 'all_vital', 'all_new_disengaged', 'others'] + example: ['all_active', 'all_new_active'] + - in: query + name: ngu + required: false + schema: + type: string + example: 'Nima' + - in: query + name: sortBy + required: false + schema: + type: string + example: 'joined_at:desc,ngu:asc' + - in: query + name: limit + required: false + schema: + type: integer + format: int32 + example: 10 + default: 10 + - in: query + name: page + required: false + schema: + type: integer + format: int32 + example: 2 + default: 1 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + id: + type: integer + username: + type: string + example: 'Behzad_Rabiei' + ngu: + type: string + example: 'Behzad' + avatar: + type: string + example: 'b50adff099924dd5e6b72d13f77eb9d7' + activityComposition: + type: array + items: + type: string + example: ['Newly active', 'Vital'] + joined_at: + type: string + format: date-time + example: '2022-05-30T15:46:52.924+00:00' + limit: + type: integer + example: 10 + page: + type: integer + example: 1 + totalPages: + type: integer + example: 2 + totalResults: + type: integer + example: 20 + '400': + description: Bad Request + $ref: '#/components/responses/BadRequest' + '401': + description: Unauthorized + $ref: '#/components/responses/Unauthorized' + '404': + description: NotFound + $ref: '#/components/responses/NotFound' + /api/v1/discourse/member-activity/{platformId}/active-members-onboarding-table: + post: + tags: + - [Discourse] + summary: Get data for active members onboarding table + security: + - bearerAuth: [] + description: for now sortBy just can apply for ngu and joined_at(DaoMemberSince in UI) + parameters: + - in: path + name: platformId + required: true + schema: + type: string + description: Platform Id + - in: query + name: activityComposition + schema: + type: array + items: + type: string + enum: ['all_joined', 'all_new_active', 'all_still_active', 'all_dropped', 'others'] + example: ['all_joined', 'all_new_active'] + - in: query + name: ngu + required: false + schema: + type: string + example: 'Nima' + - in: query + name: sortBy + required: false + schema: + type: string + example: 'joined_at:desc,ngu:asc' + - in: query + name: limit + required: false + schema: + type: integer + format: int32 + example: 10 + default: 10 + - in: query + name: page + required: false + schema: + type: integer + format: int32 + example: 2 + default: 1 + + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + id: + type: integer + username: + type: string + example: 'Behzad_Rabiei' + ngu: + type: string + example: 'Behzad' + avatar: + type: string + example: 'b50adff099924dd5e6b72d13f77eb9d7' + activityComposition: + type: array + items: + type: string + example: ['Newly active', 'Dropped'] + joined_at: + type: string + format: date-time + example: '2022-05-30T15:46:52.924+00:00' + limit: + type: integer + example: 10 + page: + type: integer + example: 1 + totalPages: + type: integer + example: 2 + totalResults: + type: integer + example: 20 + '400': + description: Bad Request + $ref: '#/components/responses/BadRequest' + '401': + description: Unauthorized + $ref: '#/components/responses/Unauthorized' + '404': + description: NotFound + $ref: '#/components/responses/NotFound' + /api/v1/discourse/member-activity/{platformId}/disengaged-members-composition-table: + post: + tags: + - [Discourse] + summary: Get data for disengaged members composition table + security: + - bearerAuth: [] + description: for now sortBy just can apply for ngu and joined_at(DaoMemberSince in UI) + parameters: + - in: path + name: platformId + required: true + schema: + type: string + description: Platform Id + - in: query + name: activityComposition + schema: + type: array + items: + type: string + enum: + [ + 'all_new_disengaged', + 'all_disengaged_were_newly_active', + 'all_disengaged_were_consistenly_active', + 'all_disengaged_were_vital', + 'others', + ] + example: ['all_new_disengaged', 'all_disengaged_were_newly_active'] + style: form + explode: true + - in: query + name: ngu + required: false + schema: + type: string + example: 'Nima' + - in: query + name: sortBy + required: false + schema: + type: string + example: 'joined_at:desc,ngu:asc' + - in: query + name: limit + required: false + schema: + type: integer + format: int32 + example: 10 + default: 10 + - in: query + name: page + required: false + schema: + type: integer + format: int32 + example: 2 + default: 1 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + id: + type: integer + username: + type: string + example: 'Behzad_Rabiei' + ngu: + type: string + example: 'Behzad' + avatar: + type: string + example: 'b50adff099924dd5e6b72d13f77eb9d7' + activityComposition: + type: array + items: + type: string + example: ['Were vital members', 'Were newly active'] + joined_at: + type: string + format: date-time + example: '2022-05-30T15:46:52.924+00:00' + limit: + type: integer + example: 10 + page: + type: integer + example: 1 + totalPages: + type: integer + example: 2 + totalResults: + type: integer + example: 20 + '400': + description: Bad Request + $ref: '#/components/responses/BadRequest' + '401': + description: Unauthorized + $ref: '#/components/responses/Unauthorized' + '404': + description: NotFound + $ref: '#/components/responses/NotFound' diff --git a/src/docs/platform.doc.yml b/src/docs/platform.doc.yml index 72bc71b..e919b01 100644 --- a/src/docs/platform.doc.yml +++ b/src/docs/platform.doc.yml @@ -185,6 +185,8 @@ paths: type: array items: type: integer + isInProgress: + type: boolean description: Metadata for Discourse. responses: '201': diff --git a/src/routes/v1/discourse.route.ts b/src/routes/v1/discourse.route.ts index 93d1225..73daf1f 100644 --- a/src/routes/v1/discourse.route.ts +++ b/src/routes/v1/discourse.route.ts @@ -26,4 +26,23 @@ router.post( discourseController.membersInteractionsNetworkGraph, ); +router.post( + '/member-activity/:platformId/active-members-composition-table', + auth('admin', 'view'), + validate(discourseValidation.activeMembersCompositionTable), + discourseController.activeMembersCompositionTable, +); +router.post( + '/member-activity/:platformId/active-members-onboarding-table', + auth('admin', 'view'), + validate(discourseValidation.activeMembersOnboardingTable), + discourseController.activeMembersOnboardingTable, +); +router.post( + '/member-activity/:platformId/disengaged-members-composition-table', + auth('admin', 'view'), + validate(discourseValidation.disengagedMembersCompositionTable), + discourseController.disengagedMembersCompositionTable, +); + export default router; diff --git a/src/services/discourse/core.service.ts b/src/services/discourse/core.service.ts index ebd2cc0..8a0832b 100644 --- a/src/services/discourse/core.service.ts +++ b/src/services/discourse/core.service.ts @@ -3,7 +3,7 @@ import { IAuthAndPlatform } from '../../interfaces'; import categoryService from './category.service'; import { ApiError, pick, sort } from '../../utils'; -const logger = parentLogger.child({ module: 'DiscordCoreService' }); +const logger = parentLogger.child({ module: 'DiscourseCoreService' }); async function getPropertyHandler(req: IAuthAndPlatform) { const filter = pick(req.query, ['name']); @@ -13,6 +13,36 @@ async function getPropertyHandler(req: IAuthAndPlatform) { } } +/** + * create discourse forum + * @param {String} endpoint + * @returns {Promise} + */ +async function createDiscourseForum(endpoint: string): Promise { + try { + const data = { + endpoint, + }; + console.log(data); + const response = await fetch('http://discourse/forums', { + method: 'POST', + body: new URLSearchParams(data), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + if (response.ok) { + console.log(await response.json()); + return; + } else { + const errorResponse = await response.text(); + logger.error({ error: errorResponse }); + } + } catch (error) { + logger.error(error, 'Failed to create discourse forum'); + throw new ApiError(590, 'Failed to create discourse forum'); + } +} + export default { getPropertyHandler, + createDiscourseForum, }; diff --git a/src/services/discourse/index.ts b/src/services/discourse/index.ts index 16e0a86..36f68f8 100644 --- a/src/services/discourse/index.ts +++ b/src/services/discourse/index.ts @@ -2,9 +2,11 @@ import heatmapService from './heatmap.service'; import categoryService from './category.service'; import coreService from './core.service'; import memberActivityService from './memberActivity.service'; +import membersService from './members.service'; export default { heatmapService, coreService, categoryService, memberActivityService, + membersService, }; diff --git a/src/services/discourse/memberActivity.service.ts b/src/services/discourse/memberActivity.service.ts index a1df73a..11a552a 100644 --- a/src/services/discourse/memberActivity.service.ts +++ b/src/services/discourse/memberActivity.service.ts @@ -1,3 +1,4 @@ +import { Connection } from 'mongoose'; import NodeStats from '../../utils/enums/nodeStats.enum'; import * as Neo4j from '../../neo4j'; import parentLogger from '../../config/logger'; @@ -6,15 +7,8 @@ import { SupportedNeo4jPlatforms } from '../../types/neo4j.type'; import { DatabaseManager } from '@togethercrew.dev/db'; import { ApiError } from '../../utils'; import httpStatus from 'http-status'; -const logger = parentLogger.child({ module: 'DiscourseMemberActivityService' }); -type networkGraphUserInformationType = { - username: string; - avatar: string | null | undefined; - joinedAt: Date | null; - roles: []; - ngu: string; -}; +const logger = parentLogger.child({ module: 'DiscourseMemberActivityService' }); function getNgu(user: any): string { if (user.options.name !== '') { @@ -24,6 +18,14 @@ function getNgu(user: any): string { } } +type networkGraphUserInformationType = { + username: string; + avatar: string | null | undefined; + joinedAt: Date | null; + roles: []; + ngu: string; +}; + function getUserInformationForNetworkGraph(user: any): networkGraphUserInformationType { return { username: user.options.username, @@ -154,7 +156,115 @@ async function getMembersInteractionsNetworkGraph( throw new ApiError(httpStatus.INTERNAL_SERVER_ERROR, 'Failed to get discourse members interaction network graph'); } } +/** + * Constructs a projection stage object for MongoDB aggregation pipeline based on the provided activity composition fields. + * + * @param {Array} fields - The activity composition fields to include in the projection. Each field corresponds to a property in the database documents. + * @returns {Stage} The projection stage object. It includes a '_id' field set to '0', an 'all' field with an empty '$setUnion', and additional fields based on the 'fields' parameter. Each additional field is prefixed with a '$'. + */ +function buildProjectStageBasedOnActivityComposition(fields: Array) { + const initialStage: { + _id: string; + all: { $setUnion: Array }; + [key: string]: string | { $setUnion: Array }; + } = { + _id: '0', + all: { $setUnion: [] }, + }; + + const finalStage = fields.reduce((stage, field) => { + stage[field] = `$${field}`; + stage.all.$setUnion.push(`$${field}`); + return stage; + }, initialStage); + + return finalStage; +} + +/** + * get activity composition fileds of active member onboarding table + * @returns {Object} + */ +function getActivityCompositionOfActiveMembersComposition() { + return ['all_active', 'all_new_active', 'all_consistent', 'all_vital', 'all_new_disengaged']; +} + +/** + * get activity composition fileds of active member compostion table + * @returns {Object} + */ +function getActivityCompositionOfActiveMembersOnboarding() { + return ['all_joined', 'all_new_active', 'all_still_active', 'all_dropped']; +} + +/** + * get activity composition fileds of disengaged member compostion table + * @returns {Object} + */ +function getActivityCompositionOfDisengagedComposition() { + return [ + 'all_new_disengaged', + 'all_disengaged_were_newly_active', + 'all_disengaged_were_consistently_active', + 'all_disengaged_were_vital', + ]; +} + +/** + * get last member activity document for usage of member activity table + * @param {Connection} platformConnection + * @param {Any} activityComposition + * @returns {Object} + */ +async function getLastDocumentForTablesUsage(platformConnection: Connection, activityComposition: Array) { + const projectStage = buildProjectStageBasedOnActivityComposition(activityComposition); + const lastDocument = await platformConnection.models.MemberActivity.aggregate([ + { $sort: { date: -1 } }, + { $limit: 1 }, + { $project: projectStage }, + ]); + return lastDocument[0]; +} + +function getActivityComposition(discourseMember: any, memberActivity: any, activityComposition: Array) { + const activityTypes = [ + { key: 'all_new_active', message: 'Newly active' }, + { key: 'all_new_disengaged', message: 'Became disengaged' }, + { key: 'all_active', message: 'Active members' }, + { key: 'all_consistent', message: 'Consistently active' }, + { key: 'all_vital', message: 'Vital member' }, + { key: 'all_joined', message: 'Joined' }, + { key: 'all_dropped', message: 'Dropped' }, + { key: 'all_still_active', message: 'Still active' }, + { key: 'all_disengaged_were_newly_active', message: 'Were newly active' }, + { key: 'all_disengaged_were_consistently_active', message: 'Were consistenly active' }, + { key: 'all_disengaged_were_vital', message: 'Were vital members' }, + ]; + + const activityCompositions = []; + + activityTypes.forEach((activityType) => { + if ( + memberActivity[activityType.key] && + memberActivity[activityType.key].includes(discourseMember.id) && + (!activityComposition || activityComposition.length === 0 || activityComposition.includes(activityType.key)) + ) { + activityCompositions.push(activityType.message); + } + }); + + if (activityCompositions.length === 0) { + activityCompositions.push('Others'); + } + + return activityCompositions; +} export default { getMembersInteractionsNetworkGraph, + getLastDocumentForTablesUsage, + getActivityComposition, + getActivityCompositionOfActiveMembersComposition, + getActivityCompositionOfActiveMembersOnboarding, + getActivityCompositionOfDisengagedComposition, }; diff --git a/src/services/discourse/members.service.ts b/src/services/discourse/members.service.ts new file mode 100644 index 0000000..f9a02fc --- /dev/null +++ b/src/services/discourse/members.service.ts @@ -0,0 +1,157 @@ +import { Connection } from 'mongoose'; +import { IHeatmapChartRequestBody } from '../../interfaces/Request.interface'; +import { date, math, sort } from '../../utils'; +import parentLogger from '../../config/logger'; + +const logger = parentLogger.child({ module: 'DiscourseMemberService' }); + +type Filter = { + activityComposition?: Array; + ngu?: string; +}; + +type Options = { + sortBy?: string; + limit?: string; + page?: string; +}; + +function getNgu(user: any): string { + if (user.name !== '') { + return user.name; + } else { + return user.username; + } +} +/** + * Query members with a filter and options. + * @param {Connection} platformConnection - The MongoDB connection. + * @param {Filter} filter - The filter object + * @param {Options} options - The options object with fields like 'sortBy', 'limit' and 'page'. + * @param {any} memberActivity - The document containing the last member activity. + * @param {Array} activityCompostionsTypes - An array containing types of activity compositions. + * @returns {Promise} - An object with the query results and other information like 'limit', 'page', 'totalPages', 'totalResults'. + */ +async function queryMembersForTables( + platformConnection: Connection, + filter: Filter, + options: Options, + memberActivity: any, + activityCompostionsTypes: Array, +) { + try { + const { ngu, activityComposition } = filter; + const { sortBy } = options; + const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10; + const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1; + const sortParams: Record = sortBy ? sort.sortByHandler(sortBy) : { 'options.username': 1 }; + + let matchStage: any = {}; + let allActivityIds: string[] = []; + + const memberActivityDocument = await platformConnection.models.MemberActivity.findOne() + .sort({ date: -1 }) + .select({ date: 1, _id: 0 }); + + if (activityComposition && activityComposition.length > 0) { + // If 'others' is in activityComposition, we exclude all IDs that are part of other activities + if (activityComposition.includes('others')) { + allActivityIds = activityCompostionsTypes + .filter((activity) => activity !== 'others') + .flatMap((activity) => memberActivity[activity]); + + matchStage.id = { $nin: allActivityIds }; + } + + // If specific activity compositions are mentioned along with 'others', we add them separately + if (activityComposition.some((activity) => activity !== 'others')) { + const specificActivityIds = activityComposition + .filter((activity) => activity !== 'others') + .flatMap((activity) => memberActivity[activity]); + + if (matchStage.id) { + matchStage = { $or: [{ id: { $in: specificActivityIds } }, matchStage] }; + } else { + matchStage.id = { $in: specificActivityIds }; + } + } + } + + if (ngu) { + matchStage.$or = [ + { 'options.username': { $regex: ngu, $options: 'i' } }, + { 'options.name': { $regex: ngu, $options: 'i' } }, + ]; + } + + if (memberActivityDocument) { + const date = new Date(memberActivityDocument.date); + date.setHours(23, 59, 59, 999); + matchStage.joined_at = { $lte: date }; + } + + const totalResults = await platformConnection.db.collection('rawmembers').countDocuments(matchStage); + + const results = await platformConnection.db + .collection('rawmembers') + .aggregate([ + { + $match: matchStage, + }, + { + $sort: sortParams, + }, + { + $skip: limit * (page - 1), + }, + { + $limit: limit, + }, + { + $project: { + id: 1, + username: '$options.username', + avatar: '$options.avatar', + joined_at: 1, + name: '$options.name', + _id: 0, + }, + }, + ]) + .toArray(); + + const totalPages = Math.ceil(totalResults / limit); + + return { + results, + limit, + page, + totalPages, + totalResults, + }; + } catch (error) { + logger.error( + { + platformConnection: platformConnection.name, + filter, + options, + memberActivity, + activityCompostionsTypes, + error, + }, + 'Failed to query members', + ); + return { + results: [], + limit: 10, + page: 1, + totalPages: 0, + totalResults: 0, + }; + } +} + +export default { + queryMembersForTables, + getNgu, +}; diff --git a/src/services/platform.service.ts b/src/services/platform.service.ts index 054fb61..3a6f73a 100644 --- a/src/services/platform.service.ts +++ b/src/services/platform.service.ts @@ -3,6 +3,7 @@ import httpStatus from 'http-status'; import { Platform, IPlatform } from '@togethercrew.dev/db'; import ApiError from '../utils/ApiError'; import sagaService from './saga.service'; +import discourseService from './discourse'; import { Snowflake } from 'discord.js'; import { analyzerAction, analyzerWindow } from '../config/analyzer.statics'; import { PlatformNames } from '@togethercrew.dev/db'; @@ -13,7 +14,7 @@ import { PlatformNames } from '@togethercrew.dev/db'; * @returns {Promise>} */ const createPlatform = async (PlatformBody: IPlatform): Promise> => { - if (PlatformBody.name === PlatformNames.Discord) { + if (PlatformBody.name === PlatformNames.Discord || PlatformBody.name === PlatformNames.Discourse) { if (PlatformBody.metadata) { PlatformBody.metadata = { action: analyzerAction, @@ -22,11 +23,13 @@ const createPlatform = async (PlatformBody: IPlatform): Promise { period: Joi.date().required(), analyzerStartedAt: Joi.date().required(), resources: Joi.array().items(Joi.number().empty()).required(), + isInProgress: Joi.boolean().default(true), }); };