diff --git a/.forestadmin-schema.json b/.forestadmin-schema.json index 811f3712a..b9b4042ec 100644 --- a/.forestadmin-schema.json +++ b/.forestadmin-schema.json @@ -5,43 +5,23 @@ { "defaultValue": null, "enums": null, - "field": "Image", + "field": "createdAt", "integration": null, - "inverseOf": "Experiences", + "inverseOf": null, "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": true, "isVirtual": false, - "reference": "Image.uid", - "relationship": "BelongsTo", - "type": "String", + "reference": null, + "type": "Date", "validations": [] }, { "defaultValue": null, "enums": null, - "field": "Member", - "integration": null, - "inverseOf": "Experiences", - "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": "companyName", + "field": "email", "integration": null, "inverseOf": null, "isFilterable": true, @@ -56,56 +36,6 @@ {"type": "is present", "message": "Failed validation rule: 'Present'"} ] }, - { - "defaultValue": false, - "enums": null, - "field": "currentTeam", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Boolean", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "description", - "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": "endDate", - "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, @@ -125,91 +55,7 @@ { "defaultValue": null, "enums": null, - "field": "startDate", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Date", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "title", - "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'"} - ] - } - ], - "icon": null, - "integration": null, - "isReadOnly": false, - "isSearchable": true, - "isVirtual": false, - "name": "Experience", - "onlyForRelationships": false, - "paginationType": "page", - "segments": [] - }, - { - "actions": [], - "fields": [ - { - "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": "email", + "field": "question", "integration": null, "inverseOf": null, "isFilterable": true, @@ -227,23 +73,7 @@ { "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": null, - "enums": null, - "field": "question", + "field": "requestIp", "integration": null, "inverseOf": null, "isFilterable": true, @@ -261,7 +91,7 @@ { "defaultValue": null, "enums": null, - "field": "requestIp", + "field": "type", "integration": null, "inverseOf": null, "isFilterable": true, @@ -439,23 +269,6 @@ { "actions": [], "fields": [ - { - "defaultValue": null, - "enums": null, - "field": "Experiences", - "integration": null, - "inverseOf": "Image", - "isFilterable": false, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": false, - "isSortable": false, - "isVirtual": false, - "reference": "Experience.uid", - "relationship": "HasMany", - "type": ["String"], - "validations": [] - }, { "defaultValue": null, "enums": null, @@ -1443,24 +1256,24 @@ { "defaultValue": null, "enums": null, - "field": "Experiences", + "field": "Image", "integration": null, - "inverseOf": "Member", - "isFilterable": false, + "inverseOf": "Members", + "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, - "isSortable": false, + "isSortable": true, "isVirtual": false, - "reference": "Experience.uid", - "relationship": "HasMany", - "type": ["String"], + "reference": "Image.uid", + "relationship": "BelongsTo", + "type": "String", "validations": [] }, { "defaultValue": null, "enums": null, - "field": "Image", + "field": "Location", "integration": null, "inverseOf": "Members", "isFilterable": true, @@ -1469,7 +1282,7 @@ "isRequired": false, "isSortable": true, "isVirtual": false, - "reference": "Image.uid", + "reference": "Location.uid", "relationship": "BelongsTo", "type": "String", "validations": [] @@ -1477,18 +1290,18 @@ { "defaultValue": null, "enums": null, - "field": "Location", + "field": "ProjectContributions", "integration": null, - "inverseOf": "Members", - "isFilterable": true, + "inverseOf": "Member", + "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, - "isSortable": true, + "isSortable": false, "isVirtual": false, - "reference": "Location.uid", - "relationship": "BelongsTo", - "type": "String", + "reference": "ProjectContribution.uid", + "relationship": "HasMany", + "type": ["String"], "validations": [] }, { @@ -2349,6 +2162,23 @@ {"type": "is present", "message": "Failed validation rule: 'Present'"} ] }, + { + "defaultValue": null, + "enums": null, + "field": "ProjectContributions", + "integration": null, + "inverseOf": "Project", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "ProjectContribution.uid", + "relationship": "HasMany", + "type": ["String"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -2368,6 +2198,23 @@ {"type": "is present", "message": "Failed validation rule: 'Present'"} ] }, + { + "defaultValue": null, + "enums": null, + "field": "_contributingTeams", + "integration": null, + "inverseOf": "Project", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "_contributingTeams.id", + "relationship": "HasMany", + "type": ["Number"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -2436,6 +2283,24 @@ "type": "Number", "validations": [] }, + { + "defaultValue": false, + "enums": null, + "field": "isDeleted", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Boolean", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, { "defaultValue": null, "enums": null, @@ -2583,6 +2448,176 @@ "paginationType": "page", "segments": [] }, + { + "actions": [], + "fields": [ + { + "defaultValue": null, + "enums": null, + "field": "Member", + "integration": null, + "inverseOf": "ProjectContributions", + "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": "Project", + "integration": null, + "inverseOf": "ProjectContributions", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "Project.uid", + "relationship": "BelongsTo", + "type": "String", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": false, + "enums": null, + "field": "currentProject", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Boolean", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "description", + "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": "endDate", + "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": null, + "enums": null, + "field": "role", + "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": "startDate", + "integration": null, + "inverseOf": null, + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": null, + "type": "Date", + "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'"} + ] + } + ], + "icon": null, + "integration": null, + "isReadOnly": false, + "isSearchable": true, + "isVirtual": false, + "name": "ProjectContribution", + "onlyForRelationships": false, + "paginationType": "page", + "segments": [] + }, { "actions": [], "fields": [ @@ -2849,6 +2884,23 @@ "type": ["Number"], "validations": [] }, + { + "defaultValue": null, + "enums": null, + "field": "_contributingTeams", + "integration": null, + "inverseOf": "Team", + "isFilterable": false, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": false, + "isSortable": false, + "isVirtual": false, + "reference": "_contributingTeams.id", + "relationship": "HasMany", + "type": ["Number"], + "validations": [] + }, { "defaultValue": null, "enums": null, @@ -3665,6 +3717,58 @@ "paginationType": "page", "segments": [] }, + { + "actions": [], + "fields": [ + { + "defaultValue": null, + "enums": null, + "field": "Project", + "integration": null, + "inverseOf": "_contributingTeams", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "Project.id", + "relationship": "BelongsTo", + "type": "Number", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + }, + { + "defaultValue": null, + "enums": null, + "field": "Team", + "integration": null, + "inverseOf": "_contributingTeams", + "isFilterable": true, + "isPrimaryKey": false, + "isReadOnly": false, + "isRequired": true, + "isSortable": true, + "isVirtual": false, + "reference": "Team.id", + "relationship": "BelongsTo", + "type": "Number", + "validations": [ + {"type": "is present", "message": "Failed validation rule: 'Present'"} + ] + } + ], + "icon": null, + "integration": null, + "isReadOnly": false, + "isSearchable": true, + "isVirtual": false, + "name": "_contributingTeams", + "onlyForRelationships": false, + "paginationType": "page", + "segments": [] + }, { "actions": [], "fields": [ diff --git a/apps/web-api/prisma/fixtures/project.ts b/apps/web-api/prisma/fixtures/project.ts index b83aaf18a..59be2d178 100644 --- a/apps/web-api/prisma/fixtures/project.ts +++ b/apps/web-api/prisma/fixtures/project.ts @@ -3,7 +3,9 @@ import { faker } from '@faker-js/faker'; import { Prisma, Project } from '@prisma/client'; import { Factory } from 'fishery'; import camelCase from 'lodash/camelCase'; +import random from 'lodash/random'; import sample from 'lodash/sample'; +import sampleSize from 'lodash/sampleSize'; import { prisma } from './../index'; const getUidsFrom = async (model, where = {}) => { @@ -20,7 +22,7 @@ const ProjectFactory = Factory.define>( onCreate(async (project) => { const teamUids = await (await getUidsFrom(Prisma.ModelName.Team)) .map((result) => result.uid); - project.teamUid = sample(teamUids) || ''; + project.maintainingTeamUid = sample(teamUids) || ''; const memberUids = await (await getUidsFrom(Prisma.ModelName.Member)) .map((result) => result.uid); project.createdBy = sample(memberUids) || ''; @@ -36,12 +38,12 @@ const ProjectFactory = Factory.define>( name: name, tagline: faker.lorem.words(3), description: faker.lorem.paragraph(), - contactEmail: faker.internet.email(), + contactEmail: faker.internet.email().toLowerCase(), lookingForFunding: false, kpis: [{ key: faker.random.word(), value: faker.random.word()}], readMe: faker.lorem.paragraph(), createdBy: '', - teamUid: '', + maintainingTeamUid: '', projectLinks: [{ name: faker.company.name(), url: faker.internet.url() @@ -51,9 +53,28 @@ const ProjectFactory = Factory.define>( url: faker.internet.url() }], createdAt: faker.date.past(), - updatedAt: faker.date.recent() + updatedAt: faker.date.recent(), + isDeleted: false }; } ); -export const projects = async () => await ProjectFactory.createList(300); +export const projects = async () => await ProjectFactory.createList(25); + +export const projectRelations = async (projects) => { + const teamUids = await getUidsFrom(Prisma.ModelName.Team); + + return projects.map((project) => { + const randomTeams = sampleSize(teamUids, random(0, 5)); + return { + where: { + uid: project.uid, + }, + data: { + ...(randomTeams.length && { + contributingTeams: { connect: randomTeams }, + }) + }, + }; + }); +}; diff --git a/apps/web-api/prisma/migrations/20231102113053_member_experience/migration.sql b/apps/web-api/prisma/migrations/20231102113053_member_experience/migration.sql deleted file mode 100644 index dc31aa4f2..000000000 --- a/apps/web-api/prisma/migrations/20231102113053_member_experience/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ --- CreateTable -CREATE TABLE "Experience" ( - "id" SERIAL NOT NULL, - "uid" TEXT NOT NULL, - "companyName" TEXT NOT NULL, - "logoUid" TEXT, - "title" TEXT NOT NULL, - "currentTeam" BOOLEAN NOT NULL DEFAULT false, - "startDate" TIMESTAMP(3) NOT NULL, - "endDate" TIMESTAMP(3) NOT NULL, - "description" TEXT, - "memberUid" TEXT NOT NULL, - - CONSTRAINT "Experience_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Experience_uid_key" ON "Experience"("uid"); - --- AddForeignKey -ALTER TABLE "Experience" ADD CONSTRAINT "Experience_logoUid_fkey" FOREIGN KEY ("logoUid") REFERENCES "Image"("uid") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Experience" ADD CONSTRAINT "Experience_memberUid_fkey" FOREIGN KEY ("memberUid") REFERENCES "Member"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/web-api/prisma/migrations/20231103093835_member_experience_end_date_optional/migration.sql b/apps/web-api/prisma/migrations/20231103093835_member_experience_end_date_optional/migration.sql deleted file mode 100644 index 95e720332..000000000 --- a/apps/web-api/prisma/migrations/20231103093835_member_experience_end_date_optional/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Experience" ALTER COLUMN "endDate" DROP NOT NULL; diff --git a/apps/web-api/prisma/migrations/20231106095859_project_contributions/migration.sql b/apps/web-api/prisma/migrations/20231106095859_project_contributions/migration.sql new file mode 100644 index 000000000..438ff3a8b --- /dev/null +++ b/apps/web-api/prisma/migrations/20231106095859_project_contributions/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + - Added the required column `type` to the `Faq` table without a default value. This is not possible if the table is not empty. + - Added the required column `maintainingTeamUid` to the `Project` table without a default value. This is not possible if the table is not empty. + +*/ + +-- DropForeignKey +ALTER TABLE "Project" DROP CONSTRAINT "Project_teamUid_fkey"; + +-- AlterTable +ALTER TABLE "Faq" ADD COLUMN "type" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "teamUid", +ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "maintainingTeamUid" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "ProjectContribution" ( + "id" SERIAL NOT NULL, + "uid" TEXT NOT NULL, + "role" TEXT, + "description" TEXT, + "currentProject" BOOLEAN NOT NULL DEFAULT false, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3), + "memberUid" TEXT NOT NULL, + "projectUid" TEXT NOT NULL, + + CONSTRAINT "ProjectContribution_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_contributingTeams" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectContribution_uid_key" ON "ProjectContribution"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "_contributingTeams_AB_unique" ON "_contributingTeams"("A", "B"); + +-- CreateIndex +CREATE INDEX "_contributingTeams_B_index" ON "_contributingTeams"("B"); + +-- AddForeignKey +ALTER TABLE "ProjectContribution" ADD CONSTRAINT "ProjectContribution_memberUid_fkey" FOREIGN KEY ("memberUid") REFERENCES "Member"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectContribution" ADD CONSTRAINT "ProjectContribution_projectUid_fkey" FOREIGN KEY ("projectUid") REFERENCES "Project"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_maintainingTeamUid_fkey" FOREIGN KEY ("maintainingTeamUid") REFERENCES "Team"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_contributingTeams" ADD CONSTRAINT "_contributingTeams_A_fkey" FOREIGN KEY ("A") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_contributingTeams" ADD CONSTRAINT "_contributingTeams_B_fkey" FOREIGN KEY ("B") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index 9b383bafb..2884dc490 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -15,63 +15,64 @@ datasource db { /// TODO: User permissions/grants model Team { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) - name String @unique - logo Image? @relation(fields: [logoUid], references: [uid]) - logoUid String? - blog String? - officeHours String? - website String? - contactMethod String? - twitterHandler String? - linkedinHandler String? - telegramHandler String? - shortDescription String? - longDescription String? - plnFriend Boolean @default(false) - airtableRecId String? @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - teamMemberRoles TeamMemberRole[] - industryTags IndustryTag[] - membershipSources MembershipSource[] - fundingStage FundingStage? @relation(fields: [fundingStageUid], references: [uid]) - fundingStageUid String? - technologies Technology[] - moreDetails String? - Project Project[] + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) + name String @unique + logo Image? @relation(fields: [logoUid], references: [uid]) + logoUid String? + blog String? + officeHours String? + website String? + contactMethod String? + twitterHandler String? + linkedinHandler String? + telegramHandler String? + shortDescription String? + longDescription String? + plnFriend Boolean @default(false) + airtableRecId String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + teamMemberRoles TeamMemberRole[] + industryTags IndustryTag[] + membershipSources MembershipSource[] + fundingStage FundingStage? @relation(fields: [fundingStageUid], references: [uid]) + fundingStageUid String? + technologies Technology[] + moreDetails String? + maintainingProjects Project[] @relation("maintainingTeam") + contributingProjects Project[] @relation("contributingTeams") } model Member { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) - name String - email String? @unique - image Image? @relation(fields: [imageUid], references: [uid]) - imageUid String? - githubHandler String? - discordHandler String? - twitterHandler String? - linkedinHandler String? - telegramHandler String? - officeHours String? - moreDetails String? - plnFriend Boolean @default(false) - plnStartDate DateTime? - airtableRecId String? @unique - externalId String? @unique - openToWork Boolean? @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - skills Skill[] - location Location? @relation(fields: [locationUid], references: [uid]) - locationUid String? - teamMemberRoles TeamMemberRole[] - memberRoles MemberRole[] - preferences Json? - experience Experience[] - Project Project[] + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) + name String + email String? @unique + image Image? @relation(fields: [imageUid], references: [uid]) + imageUid String? + githubHandler String? + discordHandler String? + twitterHandler String? + linkedinHandler String? + telegramHandler String? + officeHours String? + moreDetails String? + plnFriend Boolean @default(false) + plnStartDate DateTime? + airtableRecId String? @unique + externalId String? @unique + openToWork Boolean? @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + skills Skill[] + location Location? @relation(fields: [locationUid], references: [uid]) + locationUid String? + teamMemberRoles TeamMemberRole[] + memberRoles MemberRole[] + preferences Json? + projectContributions ProjectContribution[] + projects Project[] } model MemberRole { @@ -206,45 +207,43 @@ model Technology { } model Image { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) // The cid is not unique because this id represents an array of multiple images // with different sizes that can be accessed trough the cid and filename - cid String - width Int - height Int - url String - filename String - size Int - type String - version ImageSize + cid String + width Int + height Int + url String + filename String + size Int + type String + version ImageSize // This image can be a thumbnail to other image - thumbnailToUid String? - thumbnailTo Image? @relation("ImageThumbnails", fields: [thumbnailToUid], references: [uid]) + thumbnailToUid String? + thumbnailTo Image? @relation("ImageThumbnails", fields: [thumbnailToUid], references: [uid]) // This image can have multiple thumbnails - thumbnails Image[] @relation("ImageThumbnails") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + thumbnails Image[] @relation("ImageThumbnails") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Reverse Relations: - Team Team[] - Member Member[] - Project Project[] - Experience Experience[] + Team Team[] + Member Member[] + Project Project[] } -model Experience { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) - companyName String - logoUid String? - companyLogo Image? @relation(fields: [logoUid], references: [uid]) - title String - currentTeam Boolean @default(false) - startDate DateTime - endDate DateTime? - description String? - memberUid String - member Member? @relation(fields: [memberUid], references: [uid]) +model ProjectContribution { + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) + role String? + description String? + currentProject Boolean @default(false) + startDate DateTime + endDate DateTime? + memberUid String + member Member? @relation(fields: [memberUid], references: [uid]) + projectUid String + project Project? @relation(fields: [projectUid], references: [uid]) } model Faq { @@ -252,6 +251,7 @@ model Faq { uid String @unique @default(cuid()) email String @db.VarChar(100) question String + type String requestIp String @db.VarChar(35) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -267,24 +267,27 @@ model JoinRequest { } model Project { - id Int @id @default(autoincrement()) - uid String @unique @default(cuid()) - logo Image? @relation(fields: [logoUid], references: [uid]) - logoUid String? - name String - tagline String - description String - contactEmail String - lookingForFunding Boolean @default(false) - projectLinks Json? - kpis Json? - readMe String? - creator Member? @relation(fields: [createdBy], references: [uid]) - createdBy String - team Team? @relation(fields: [teamUid], references: [uid]) - teamUid String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + uid String @unique @default(cuid()) + logo Image? @relation(fields: [logoUid], references: [uid]) + logoUid String? + name String + tagline String + description String + contactEmail String + lookingForFunding Boolean @default(false) + projectLinks Json? + kpis Json? + readMe String? + creator Member? @relation(fields: [createdBy], references: [uid]) + createdBy String + maintainingTeam Team? @relation("maintainingTeam", fields: [maintainingTeamUid], references: [uid]) + maintainingTeamUid String + contributingTeams Team[] @relation("contributingTeams") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + projectContributions ProjectContribution[] + isDeleted Boolean @default(false) } enum ImageSize { diff --git a/apps/web-api/prisma/seed.ts b/apps/web-api/prisma/seed.ts index 29d929c82..31e280f1f 100644 --- a/apps/web-api/prisma/seed.ts +++ b/apps/web-api/prisma/seed.ts @@ -18,7 +18,8 @@ import { teams, technologies, memberRoles, - projects + projects, + projectRelations } from './fixtures'; /** @@ -98,7 +99,10 @@ load([ }, }, { [Prisma.ModelName.TeamMemberRole]: teamMemberRoles }, - { [Prisma.ModelName.Project]: projects} + { [Prisma.ModelName.Project]: { + fixtures: projects, + relations: projectRelations + }} ]) .then(async () => { await prisma.$disconnect(); diff --git a/apps/web-api/src/faq/faq.service.ts b/apps/web-api/src/faq/faq.service.ts index 3fa5c52f3..345bfdd63 100644 --- a/apps/web-api/src/faq/faq.service.ts +++ b/apps/web-api/src/faq/faq.service.ts @@ -26,6 +26,7 @@ export class FaqService { data: { email: question.email, question: question.question, + type: question.type, requestIp: requestIP } }); diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index d8029dd59..7ba3e7e59 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -65,7 +65,15 @@ export class MembersService { }, }, }, - experience: {include: {companyLogo : true}} + projectContributions: { + include: { + project: { + include:{ + logo: true + } + } + } + } }, }); } @@ -77,7 +85,7 @@ export class MembersService { image: true, memberRoles: true, teamMemberRoles: true, - experience: true + projectContributions: true }, }); } @@ -89,7 +97,7 @@ export class MembersService { image: true, memberRoles: true, teamMemberRoles: true, - experience: true + projectContributions: true }, }); } diff --git a/apps/web-api/src/participants-request/participants-request.service.ts b/apps/web-api/src/participants-request/participants-request.service.ts index 4f935680b..ab1d5e2c0 100644 --- a/apps/web-api/src/participants-request/participants-request.service.ts +++ b/apps/web-api/src/participants-request/participants-request.service.ts @@ -309,7 +309,6 @@ export class ParticipantsRequestService { dataToSave['moreDetails'] = dataToProcess.moreDetails; dataToSave['plnStartDate'] = dataToProcess.plnStartDate; dataToSave['openToWork'] = dataToProcess.openToWork; - //dataToSave['experience'] = dataToProcess.experience; // Team member roles relational mapping dataToSave['teamMemberRoles'] = { @@ -326,10 +325,11 @@ export class ParticipantsRequestService { }; // Save Experience if available - if(dataToProcess.experience && Array.isArray(dataToProcess.experience) && dataToProcess.experience.length > 0) { - dataToSave['experience'] = { + if(Array.isArray(dataToProcess.projectContributions) + && dataToProcess.projectContributions?.length > 0) { + dataToSave['projectContributions'] = { createMany: { - data: dataToProcess.experience + data: dataToProcess.projectContributions }, }; } @@ -429,7 +429,7 @@ export class ParticipantsRequestService { skills: true, teamMemberRoles: true, memberRoles: true, - experience: true + projectContributions: true }, }); const dataToProcess = dataFromDB?.newData; @@ -645,48 +645,46 @@ export class ParticipantsRequestService { }), }); - const expToCreate: any = [...dataToProcess.experience].filter(exp => !exp.uid) - const expIdsTodelete:any = []; - const expIdsToUpdate:any = []; + const contributionsToCreate: any = dataToProcess.projectContributions + ?.filter(contribution => !contribution.uid); + const contributionIdsToDelete:any = []; + const contributionIdsToUpdate:any = []; + const contributionIds = dataToProcess.projectContributions + ?.filter(contribution => contribution.uid).map(contribution => contribution.uid); - - const expIds = [...dataToProcess.experience].filter(exp => exp.uid).map(v => v.uid); - - existingData.experience?.map((exp:any)=> { - if(!expIds.includes(exp.uid)) { - expIdsTodelete.push(exp.uid); + existingData.projectContributions?.map((contribution:any)=> { + if(!contributionIds.includes(contribution.uid)) { + contributionIdsToDelete.push(contribution.uid); } else { - expIdsToUpdate.push(exp.uid); + contributionIdsToUpdate.push(contribution.uid); } }); - - const experienceToDelete = expIdsTodelete.map((uid) => - tx.experience.delete({ + const contributionToDelete = contributionIdsToDelete.map((uid) => + tx.projectContribution.delete({ where: { uid } }) ); - - const expsToUpdate = dataToProcess.experience.filter(v => expIdsToUpdate.includes(v.uid)) - const experienceToUpdate = expsToUpdate.map((exp) => - tx.experience.update({ + const contributions = dataToProcess.projectContributions. + filter(contribution => contributionIdsToUpdate.includes(contribution.uid)); + const contributionsToUpdate = contributions.map((contribution) => + tx.projectContribution.update({ where: { - uid: exp.uid + uid: contribution.uid }, data: { - ...exp + ...contribution } }) ); - await Promise.all(experienceToDelete); - await Promise.all(experienceToUpdate); - - await tx.experience.createMany({ - data: expToCreate.map((exp) => { - exp.memberUid = dataFromDB.referenceUid; - return exp; + await Promise.all(contributionToDelete); + await Promise.all(contributionsToUpdate); + await tx.projectContribution.createMany({ + data: contributionsToCreate.map((contribution) => { + contribution.memberUid = dataFromDB.referenceUid; + return contribution; }), }); diff --git a/apps/web-api/src/projects/projects.controller.ts b/apps/web-api/src/projects/projects.controller.ts index 81b6712f1..2d1c992e7 100644 --- a/apps/web-api/src/projects/projects.controller.ts +++ b/apps/web-api/src/projects/projects.controller.ts @@ -67,4 +67,13 @@ export class ProjectsController { } return project; } + + @Api(server.route.removeProject) + @UsePipes(ZodValidationPipe) + @UseGuards(UserTokenValidation) + remove(@Param('uid') uid: string, + @Req() request + ) { + return this.projectsService.removeProjectByUid(uid, request.userEmail); + } } diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts index 469a4a5ba..12a58b4d3 100644 --- a/apps/web-api/src/projects/projects.service.ts +++ b/apps/web-api/src/projects/projects.service.ts @@ -1,8 +1,9 @@ -import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { Inject, CACHE_MANAGER, BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { LogService } from '../shared/log.service'; import { PrismaService } from '../shared/prisma.service'; import { Prisma } from '@prisma/client'; import { MembersService } from '../members/members.service'; +import { Cache } from 'cache-manager'; @Injectable() export class ProjectsService { @@ -10,16 +11,26 @@ export class ProjectsService { private prisma: PrismaService, private memberService: MembersService, private logger: LogService, + @Inject(CACHE_MANAGER) private cacheService: Cache + ) {} async createProject(project: Prisma.ProjectUncheckedCreateInput, userEmail: string) { try { const member:any = await this.getMemberInfo(userEmail); - await this.checkDirectoryAdminOrTeamLead(member, project.teamUid); + const { contributingTeams } : any = project; project.createdBy = member.uid; - return await this.prisma.project.create({ - data: project + project.isDeleted = false; + const result = await this.prisma.project.create({ + data: { + ...project, + contributingTeams: { + connect: contributingTeams.map(team => { return { uid: team.uid }}) + } + } }); + await this.cacheService.reset(); + return result; } catch(err) { this.handleErrors(err); } @@ -32,24 +43,37 @@ export class ProjectsService { ) { try { const member:any = await this.getMemberInfo(userEmail); - await this.checkDirectoryAdminOrTeamLead(member, project.teamUid); - return await this.prisma.project.update({ + const existingData = await this.getProjectByUid(uid); + const contributingTeamsUid = existingData?.contributingTeams?.map(team => team.uid) || []; + await this.isMemberPartOfTeams(member, [ existingData?.maintainingTeamUid, ...contributingTeamsUid], existingData); + const { contributingTeams } : any = project; + const result = await this.prisma.project.update({ where: { - uid, + uid }, data: { - ...project + ...project, + contributingTeams: { + disconnect: contributingTeamsUid.map(uid => { return { uid }}), + connect: contributingTeams.map(team => { return { uid: team.uid }}) + } } }); + await this.cacheService.reset(); + return result; } catch(err) { this.handleErrors(err, `${uid}`); } } - + async getProjects(queryOptions: Prisma.ProjectFindManyArgs) { try { + queryOptions.where = { + ...queryOptions.where, + isDeleted: false + }; queryOptions.include = { - team: { select: { uid: true, name: true, logo: true }}, + maintainingTeam: { select: { uid: true, name: true, logo: true }}, creator: { select: { uid: true, name: true, image: true }}, logo: true }; @@ -66,7 +90,8 @@ export class ProjectsService { return await this.prisma.project.findUnique({ where: { uid }, include: { - team: { select: { uid: true, name: true, logo: true }}, + maintainingTeam: { select: { uid: true, name: true, logo: true }}, + contributingTeams: { select: { uid: true, name: true, logo: true }}, creator: { select: { uid: true, name: true, image: true }}, logo: true } @@ -77,12 +102,19 @@ export class ProjectsService { } async removeProjectByUid( - uid: string + uid: string, + userEmail: string ) { + const member:any = await this.getMemberInfo(userEmail); + const existingData = await this.getProjectByUid(uid); + await this.isCreatorOfProjectOrAdmin(member, existingData); try { - return await this.prisma.project.delete({ - where: { uid } + const result = await this.prisma.project.update({ + where: { uid }, + data: { isDeleted: true } }); + await this.cacheService.reset(); + return result; } catch(err) { this.handleErrors(err, `${uid}`); } @@ -111,15 +143,33 @@ export class ProjectsService { return await this.memberService.findMemberByEmail(memberEmail) }; - async checkDirectoryAdminOrTeamLead(member, teamUid) { - if (this.memberService.checkIfAdminUser(member)) { + async checkDirectoryAdminOrTeamMember(member, teamUid) { + const res = member.teamMemberRoles.filter((role) => { + return role.teamUid === teamUid + }); + if (res.length > 0 || this.memberService.checkIfAdminUser(member)) { return true; + } else { + throw new ForbiddenException(`Member ${member.uid} isn't lead to the team ${teamUid}`); } - const res = await this.memberService.isMemberLeadTeam(member, teamUid); - if (res) { - return res; + } + + async isMemberPartOfTeams(member, teams, project) { + const res = member.teamMemberRoles.some((role) => { + return teams.includes(role.teamUid) + }); + if (res || this.memberService.checkIfAdminUser(member) || member.uid === project.createdBy) { + return true; + } else { + throw new ForbiddenException(`Member ${member.uid} isn't part of the any of the teams`); + } + } + + async isCreatorOfProjectOrAdmin(member, project) { + if (member.uid === project.createdBy || this.memberService.checkIfAdminUser(member)) { + return true; } else { - throw new ForbiddenException(`Member isn't lead to the team ${teamUid}`); + throw new ForbiddenException(`Member ${member.uid} isn't creator of the project ${project.uid}`); } } } diff --git a/libs/contracts/src/lib/contract-project.ts b/libs/contracts/src/lib/contract-project.ts index eea35c790..b5fabc1a7 100644 --- a/libs/contracts/src/lib/contract-project.ts +++ b/libs/contracts/src/lib/contract-project.ts @@ -42,5 +42,14 @@ export const apiProjects = contract.router({ 200: contract.response(), }, summary: 'Modify a project', + }, + removeProject: { + method: 'DELETE', + path: `${getAPIVersionAsPath('1')}/projects/:uid`, + body: contract.body(), + responses: { + 200: contract.response(), + }, + summary: 'Remove a project', } }); diff --git a/libs/contracts/src/schema/index.ts b/libs/contracts/src/schema/index.ts index 4f0504cea..791e2fed6 100644 --- a/libs/contracts/src/schema/index.ts +++ b/libs/contracts/src/schema/index.ts @@ -12,5 +12,5 @@ export * from './team-member-role'; export * from './technology'; export * from './faq'; export * from './project'; -export * from './experience'; +export * from './project-contribution'; export * from './join-requests'; diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index e9969c053..119a04144 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -5,7 +5,7 @@ import { LocationResponseSchema } from './location'; import { QueryParams, RETRIEVAL_QUERY_FILTERS } from './query-params'; import { ResponseSkillSchema } from './skill'; import { ResponseTeamMemberRoleSchema } from './team-member-role'; -import { ExperienceSchema } from './experience'; +import { ProjectContributionSchema } from './project-contribution'; export const GitHubRepositorySchema = z.object({ name: z.string(), @@ -45,7 +45,7 @@ export const MemberSchema = z.object({ linkedinHandler: z.string().nullish(), repositories: GitHubRepositorySchema.array().optional(), preferences: PreferenceSchema.optional(), - experience: z.array(ExperienceSchema).optional() + projectContributions: z.array(ProjectContributionSchema).optional() }); diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index 63961c27a..7ad6290c7 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ExperienceSchema } from './experience'; +import { ProjectContributionSchema } from './project-contribution'; export const statusEnum = z.enum(['PENDING', 'APPROVED', 'REJECTED']); export const participantTypeEnum = z.enum(['MEMBER', 'TEAM']); @@ -47,7 +47,7 @@ const newDataMemberSchema = z.object({ officeHours: z.string().optional().nullable(), imageUid: z.string().optional().nullable(), moreDetails: z.string().optional().nullable(), - experience: z.array(ExperienceSchema).optional() + projectContributions: z.array(ProjectContributionSchema).optional() }); const newDataTeamSchema = z.object({ diff --git a/libs/contracts/src/schema/experience.ts b/libs/contracts/src/schema/project-contribution.ts similarity index 66% rename from libs/contracts/src/schema/experience.ts rename to libs/contracts/src/schema/project-contribution.ts index 64441da17..e3c2ca278 100644 --- a/libs/contracts/src/schema/experience.ts +++ b/libs/contracts/src/schema/project-contribution.ts @@ -1,19 +1,19 @@ import { z } from 'zod'; -export const ExperienceSchema = z.object({ - companyName: z.string(), - logoUid: z.string().optional(), - title: z.string(), - currentTeam: z.boolean(), - startDate: z.string(), - endDate: z.string().optional().nullable(), - description: z.string().optional(), - memberUid: z.string().optional(), - uid: z.string().optional() + +export const ProjectContributionSchema = z.object({ + role: z.string(), + currentProject: z.boolean(), + startDate: z.string(), + endDate: z.string().optional().nullable(), + description: z.string().optional(), + projectUid: z.string(), + uid: z.string().optional() }).superRefine((data, ctx) => { - if(!data.currentTeam && !data.endDate) { + + if(!data.currentProject && !data.endDate) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'End date should not be null for past experience', + message: 'End date should not be null for past contribution', fatal: true, }); } @@ -42,6 +42,5 @@ export const ExperienceSchema = z.object({ }); } - return z.never; }); diff --git a/libs/contracts/src/schema/project.ts b/libs/contracts/src/schema/project.ts index 51d93124d..e59591a03 100644 --- a/libs/contracts/src/schema/project.ts +++ b/libs/contracts/src/schema/project.ts @@ -1,22 +1,25 @@ import { createZodDto } from 'nestjs-zod'; import { z } from 'nestjs-zod/z'; + const ProjectSchema = z.object({ logoUid: z.string().optional(), name: z.string(), tagline: z.string(), description: z.string(), - contactEmail: z.string(), + contactEmail: z.string().email().transform((email)=> { return email.toLowerCase()}), lookingForFunding: z.boolean().default(false), projectLinks: z.object({ name: z.string(), url: z.string() }).array().optional(), kpis: z.object({ key: z.string(), value: z.string() }).array().optional(), - teamUid: z.string(), + maintainingTeamUid: z.string(), + contributingTeams: z.object({ uid: z.string(), name: z.string() }).array().optional(), readMe: z.string().optional() }); export const ResponseProjectWithRelationsSchema = ProjectSchema.extend({}); export const ResponseProjectSuccessSchema = z.object({ success: z.boolean()}); -export class CreateProjectDto extends createZodDto(ProjectSchema) {} export class UpdateProjectDto extends createZodDto(ProjectSchema.partial()) {} +export class CreateProjectDto extends createZodDto(ProjectSchema) {} +