diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index 88ea58a..1ec8a37 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -132,7 +132,7 @@ export interface NexusGenEnums { POST_TYPE: "Bounty" | "Question" | "Story" ProjectLaunchStatusEnum: "Launched" | "WIP" RoleLevelEnum: 3 | 0 | 1 | 2 | 4 - TEAM_MEMBER_ROLE: "Admin" | "Maker" + TEAM_MEMBER_ROLE: "Admin" | "Maker" | "Owner" TournamentEventTypeEnum: 2 | 3 | 0 | 1 TournamentMakerHackingStatusEnum: 1 | 0 VOTE_ITEM_TYPE: "Bounty" | "PostComment" | "Project" | "Question" | "Story" | "User" @@ -283,6 +283,7 @@ export interface NexusGenObjects { tagline: string; // String! telegram?: string | null; // String title: string; // String! + tournaments?: NexusGenRootTypes['TournamentProject'][] | null; // [TournamentProject!] twitter?: string | null; // String votes_count: number; // Int! website: string; // String! @@ -360,6 +361,10 @@ export interface NexusGenObjects { amount: string; // String! title: string; // String! } + TournamentProject: { // root type + project: NexusGenRootTypes['Project']; // Project! + tournament: NexusGenRootTypes['Tournament']; // Tournament! + } TournamentProjectsResponse: { // root type hasNext?: boolean | null; // Boolean hasPrev?: boolean | null; // Boolean @@ -518,7 +523,7 @@ export interface NexusGenFieldTypes { confirmVote: NexusGenRootTypes['Vote']; // Vote! createProject: NexusGenRootTypes['CreateProjectResponse'] | null; // CreateProjectResponse createStory: NexusGenRootTypes['Story'] | null; // Story - deleteProject: NexusGenRootTypes['CreateProjectResponse'] | null; // CreateProjectResponse + deleteProject: NexusGenRootTypes['Project'] | null; // Project deleteStory: NexusGenRootTypes['Story'] | null; // Story donate: NexusGenRootTypes['Donation']; // Donation! registerInTournament: NexusGenRootTypes['User'] | null; // User @@ -590,6 +595,7 @@ export interface NexusGenFieldTypes { telegram: string | null; // String thumbnail_image: string; // String! title: string; // String! + tournaments: NexusGenRootTypes['TournamentProject'][] | null; // [TournamentProject!] twitter: string | null; // String votes_count: number; // Int! website: string; // String! @@ -720,6 +726,10 @@ export interface NexusGenFieldTypes { image: string; // String! title: string; // String! } + TournamentProject: { // field return type + project: NexusGenRootTypes['Project']; // Project! + tournament: NexusGenRootTypes['Tournament']; // Tournament! + } TournamentProjectsResponse: { // field return type hasNext: boolean | null; // Boolean hasPrev: boolean | null; // Boolean @@ -904,7 +914,7 @@ export interface NexusGenFieldTypeNames { confirmVote: 'Vote' createProject: 'CreateProjectResponse' createStory: 'Story' - deleteProject: 'CreateProjectResponse' + deleteProject: 'Project' deleteStory: 'Story' donate: 'Donation' registerInTournament: 'User' @@ -976,6 +986,7 @@ export interface NexusGenFieldTypeNames { telegram: 'String' thumbnail_image: 'String' title: 'String' + tournaments: 'TournamentProject' twitter: 'String' votes_count: 'Int' website: 'String' @@ -1106,6 +1117,10 @@ export interface NexusGenFieldTypeNames { image: 'String' title: 'String' } + TournamentProject: { // field return type name + project: 'Project' + tournament: 'Tournament' + } TournamentProjectsResponse: { // field return type name hasNext: 'Boolean' hasPrev: 'Boolean' diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 920321b..efd2546 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -187,7 +187,7 @@ type Mutation { confirmVote(payment_request: String!, preimage: String!): Vote! createProject(input: CreateProjectInput): CreateProjectResponse createStory(data: StoryInputType): Story - deleteProject(id: Int!): CreateProjectResponse + deleteProject(id: Int!): Project deleteStory(id: Int!): Story donate(amount_in_sat: Int!): Donation! registerInTournament(data: RegisterInTournamentInput, tournament_id: Int!): User @@ -302,6 +302,7 @@ type Project { telegram: String thumbnail_image: String! title: String! + tournaments: [TournamentProject!] twitter: String votes_count: Int! website: String! @@ -406,6 +407,7 @@ input StoryInputType { enum TEAM_MEMBER_ROLE { Admin Maker + Owner } type Tag { @@ -494,6 +496,11 @@ type TournamentPrize { title: String! } +type TournamentProject { + project: Project! + tournament: Tournament! +} + type TournamentProjectsResponse { hasNext: Boolean hasPrev: Boolean diff --git a/api/functions/graphql/types/project.js b/api/functions/graphql/types/project.js index 4ac2e98..f16eed7 100644 --- a/api/functions/graphql/types/project.js +++ b/api/functions/graphql/types/project.js @@ -10,10 +10,13 @@ const { } = require('nexus'); const { getUserByPubKey } = require('../../../auth/utils/helperFuncs'); const { prisma } = require('../../../prisma'); +const { deleteImage } = require('../../../services/imageUpload.service'); +const { logError } = require('../../../utils/logger'); const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl'); const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers'); const { ImageInput } = require('./misc'); +const { TournamentProject } = require('./tournament'); const { MakerRole } = require('./users'); @@ -98,6 +101,9 @@ const Project = objectType({ } }) + t.list.nonNull.field('tournaments', { + type: TournamentProject + }) t.nonNull.list.nonNull.field('capabilities', { type: Capability, @@ -130,9 +136,13 @@ const Project = objectType({ } }) +const ROLE_OWNER = 'Owner' +const ROLE_ADMIN = 'Admin' +const ROLE_MAKER = 'Maker' + const TEAM_MEMBER_ROLE = enumType({ name: 'TEAM_MEMBER_ROLE', - members: ['Admin', 'Maker'], + members: [ROLE_OWNER, ROLE_ADMIN, ROLE_MAKER], }); const ProjectMember = objectType({ @@ -450,6 +460,8 @@ const CreateProjectResponse = objectType({ } }) + + const createProject = extendType({ type: 'Mutation', definition(t) { @@ -457,7 +469,7 @@ const createProject = extendType({ type: CreateProjectResponse, args: { input: CreateProjectInput }, async resolve(_root, args, ctx) { - const { + let { title, tagline, hashtag, @@ -467,6 +479,8 @@ const createProject = extendType({ cover_image, discord, github, + slack, + telegram, twitter, website, launch_status, @@ -476,54 +490,194 @@ const createProject = extendType({ thumbnail_image, tournaments, } = args.input - console.log(launch_status); - const user = await getUserByPubKey(ctx.userPubKey); + + const user = await getUserByPubKey(ctx.userPubKey) // Do some validation - if (!user) - throw new ApolloError("Not Authenticated"); + if (!user) throw new ApolloError('Not Authenticated') - // TODO Create project - } + // Many Owners found. Throw an error + if (members.filter((m) => m.role === ROLE_OWNER).length > 1) { + throw new ApolloError('Only 1 owner can be defined.') + } + + // No owner found. Set the current user as Owner + if (!members.find((m) => m.role === ROLE_OWNER)) { + const currentUser = members.find((m) => m.id === user.id) + if (currentUser) { + currentUser.role = ROLE_OWNER + } else { + members = [{ id: user.id, role: ROLE_OWNER }, ...members] + } + } + + const coverImage = await prisma.hostedImage.findFirst({ + where: { + provider_image_id: cover_image.id, + }, + }) + + const coverImageRel = coverImage + ? { + cover_image_rel: { + connect: { + id: coverImage ? coverImage.id : null, + }, + }, + } + : {} + + const thumbnailImage = await prisma.hostedImage.findFirst({ + where: { + provider_image_id: thumbnail_image.id, + }, + }) + + const thumbnailImageRel = thumbnailImage + ? { + thumbnail_image_rel: { + connect: { + id: thumbnailImage ? thumbnailImage.id : null, + }, + }, + } + : {} + + const screenshots_ids = await prisma.hostedImage.findMany({ + where: { + provider_image_id: { + in: screenshots.map((s) => s.id), + }, + }, + select: { + id: true, + }, + }) + + const project = await prisma.project.create({ + data: { + title, + description, + tagline, + hashtag, + website, + discord, + github, + twitter, + slack, + telegram, + launch_status, + + ...coverImageRel, + ...thumbnailImageRel, + screenshots_ids: screenshots_ids.map((s) => s.id), + + category: { + connect: { + id: category_id, + }, + }, + members: { + create: members.map((member) => { + return { + role: member.role, + user: { + connect: { + id: member.id, + }, + }, + } + }), + }, + recruit_roles: { + create: recruit_roles.map((role) => { + return { + level: 0, + role: { + connect: { + id: role, + }, + }, + } + }), + }, + tournaments: { + create: tournaments.map((tournament) => { + return { + tournament: { + connect: { + id: tournament, + }, + }, + } + }), + }, + capabilities: { + connect: capabilities.map((c) => { + return { + id: c, + } + }), + }, + }, + }) + + await prisma.hostedImage + .updateMany({ + where: { + id: { + in: [coverImage.id, thumbnailImage.id, ...screenshots_ids.map((s) => s.id)], + }, + }, + data: { + is_used: true, + }, + }) + .catch((error) => { + logError(error) + throw new ApolloError('Unexpected error happened...') + }) + + return { project } + }, }) }, }) - const UpdateProjectInput = inputObjectType({ name: 'UpdateProjectInput', definition(t) { t.int('id') - t.nonNull.string('title'); - t.nonNull.string('hashtag'); - t.nonNull.string('website'); - t.nonNull.string('tagline'); - t.nonNull.string('description'); + t.nonNull.string('title') + t.nonNull.string('hashtag') + t.nonNull.string('website') + t.nonNull.string('tagline') + t.nonNull.string('description') t.nonNull.field('thumbnail_image', { - type: ImageInput + type: ImageInput, }) t.nonNull.field('cover_image', { - type: ImageInput + type: ImageInput, }) - t.string('twitter'); - t.string('discord'); - t.string('github'); - t.string('slack'); - t.string('telegram'); - t.nonNull.int('category_id'); - t.nonNull.list.nonNull.int('capabilities'); + t.string('twitter') + t.string('discord') + t.string('github') + t.string('slack') + t.string('telegram') + t.nonNull.int('category_id') + t.nonNull.list.nonNull.int('capabilities') t.nonNull.list.nonNull.field('screenshots', { - type: ImageInput - }); + type: ImageInput, + }) t.nonNull.list.nonNull.field('members', { - type: TeamMemberInput - }); - t.nonNull.list.nonNull.int('recruit_roles'); // ids + type: TeamMemberInput, + }) + t.nonNull.list.nonNull.int('recruit_roles') // ids t.nonNull.field('launch_status', { - type: ProjectLaunchStatusEnum - }); - t.nonNull.list.nonNull.int('tournaments'); // ids - } + type: ProjectLaunchStatusEnum, + }) + t.nonNull.list.nonNull.int('tournaments') // ids + }, }) const updateProject = extendType({ @@ -533,8 +687,252 @@ const updateProject = extendType({ type: CreateProjectResponse, args: { input: UpdateProjectInput }, async resolve(_root, args, ctx) { + const { + id, + title, + tagline, + hashtag, + description, + capabilities, + category_id, + cover_image, + discord, + github, + slack, + telegram, + twitter, + website, + launch_status, + members, + recruit_roles, + screenshots, + thumbnail_image, + tournaments, + } = args.input - } + const user = await getUserByPubKey(ctx.userPubKey) + + // Do some validation + if (!user) throw new ApolloError('Not Authenticated') + + const project = await prisma.project.findFirst({ + where: { + id, + }, + include: { + members: true, + }, + }) + + // Maker can't project info + if (project.members.find((m) => m.userId === user.id)?.role === ROLE_MAKER) { + throw new ApolloError("Makers can't change project info") + } + + let newMembers = [] + + // Admin can only change makers + if (project.members.find((m) => m.userId === user.id)?.role === ROLE_ADMIN) { + // Changing Makers + const newMakers = members.filter((m) => m.role === ROLE_MAKER) + + // Set old Admins and Owner using current project.memebers because Admin can't change these Roles + const currentAdminsOwner = project.members + .filter((m) => m.role === ROLE_ADMIN || m.role === ROLE_OWNER) + .map((m) => ({ id: m.userId, role: m.role })) + + newMembers = [...newMakers, ...currentAdminsOwner] + } else { + // Curent user is Owner. Can change all users roles + newMembers = members + } + + let imagesToDelete = [] + let imagesToAdd = [] + + let coverImageRel = {} + if (cover_image.id) { + const coverImage = await prisma.hostedImage.findFirst({ + where: { + provider_image_id: cover_image.id, + }, + }) + + coverImageRel = coverImage + ? { + cover_image_rel: { + connect: { + id: coverImage ? coverImage.id : null, + }, + }, + } + : {} + + if (coverImage) { + imagesToAdd.push(coverImage.id) + } + + imagesToDelete.push(project.cover_image_id) + } + + let thumbnailImageRel = {} + if (thumbnail_image.id) { + const thumbnailImage = await prisma.hostedImage.findFirst({ + where: { + provider_image_id: thumbnail_image.id, + }, + }) + + thumbnailImageRel = thumbnailImage + ? { + thumbnail_image_rel: { + connect: { + id: thumbnailImage ? thumbnailImage.id : null, + }, + }, + } + : {} + + if (thumbnailImage) { + imagesToAdd.push(thumbnailImage.id) + } + + imagesToDelete.push(project.thumbnail_image_id) + } + + let screenshots_ids = [] + for (const screenshot of screenshots) { + if (screenshot.id) { + const newScreenshot = await prisma.hostedImage.findFirst({ + where: { + provider_image_id: screenshot.id, + }, + select: { + id: true, + }, + }) + if (newScreenshot) { + screenshots_ids.push(newScreenshot.id) + imagesToAdd.push(newScreenshot.id) + } + } else { + const newScreenshot = await prisma.hostedImage.findFirst({ + where: { + url: screenshot.url, + }, + select: { + id: true, + }, + }) + if (newScreenshot) { + screenshots_ids.push(newScreenshot.id) + } + } + } + const screenshotsIdsToDelete = project.screenshots_ids.filter((x) => !screenshots_ids.includes(x)) + imagesToDelete = [...imagesToDelete, ...screenshotsIdsToDelete] + + const updatedProject = await prisma.project + .update({ + where: { + id, + }, + data: { + title, + description, + tagline, + hashtag, + website, + discord, + github, + twitter, + slack, + telegram, + launch_status, + + ...coverImageRel, + ...thumbnailImageRel, + screenshots_ids, + + category: { + connect: { + id: category_id, + }, + }, + members: { + deleteMany: {}, + create: newMembers.map((member) => { + return { + role: member.role, + user: { + connect: { + id: member.id, + }, + }, + } + }), + }, + recruit_roles: { + deleteMany: {}, + create: recruit_roles.map((role) => { + return { + level: 0, + role: { + connect: { + id: role, + }, + }, + } + }), + }, + tournaments: { + deleteMany: {}, + create: tournaments.map((tournament) => { + return { + tournament: { + connect: { + id: tournament, + }, + }, + } + }), + }, + capabilities: { + set: capabilities.map((c) => { + return { + id: c, + } + }), + }, + }, + }) + .catch((error) => { + logError(error) + throw new ApolloError('Unexpected error happened...') + }) + + if (imagesToAdd.length > 0) { + await prisma.hostedImage + .updateMany({ + where: { + id: { + in: imagesToAdd, + }, + }, + data: { + is_used: true, + }, + }) + .catch((error) => { + logError(error) + throw new ApolloError('Unexpected error happened...') + }) + } + + imagesToDelete.map(async (i) => await deleteImage(i)) + + return { project: updatedProject } + }, }) }, }) @@ -543,11 +941,78 @@ const deleteProject = extendType({ type: 'Mutation', definition(t) { t.field('deleteProject', { - type: CreateProjectResponse, + type: 'Project', args: { id: nonNull(intArg()) }, async resolve(_root, args, ctx) { - // ... - } + const { id } = args + const user = await getUserByPubKey(ctx.userPubKey) + + // Do some validation + if (!user) throw new ApolloError('Not Authenticated') + + const project = await prisma.project.findFirst({ + where: { id }, + include: { + members: true, + }, + }) + + if (!project) throw new ApolloError('Project not found') + + if (project.members.find((m) => m.userId === user.id)?.role !== ROLE_OWNER) + throw new ApolloError("You don't have the right to delete this project") + + // Award is not implemented yet + // await prisma.award.deleteMany({ + // where: { + // projectId: project.id + // } + // }) + + await prisma.projectRecruitRoles.deleteMany({ + where: { + projectId: project.id, + }, + }) + + await prisma.projectMember.deleteMany({ + where: { + projectId: project.id, + }, + }) + + await prisma.tournamentProject.deleteMany({ + where: { + project_id: project.id, + }, + }) + + const deletedProject = await prisma.project.delete({ + where: { + id, + }, + }) + + const imagesToDelete = await prisma.hostedImage.findMany({ + where: { + OR: [ + { id: project.cover_image_id }, + { id: project.thumbnail_image_id }, + { + id: { + in: project.screenshots_ids, + }, + }, + ], + }, + select: { + id: true, + }, + }) + imagesToDelete.map(async (i) => await deleteImage(i.id)) + + return deletedProject + }, }) }, }) diff --git a/api/functions/graphql/types/tournament.js b/api/functions/graphql/types/tournament.js index 5d9102b..5f4f41b 100644 --- a/api/functions/graphql/types/tournament.js +++ b/api/functions/graphql/types/tournament.js @@ -76,7 +76,13 @@ const TournamentMakerHackingStatusEnum = enumType({ }, }); - +const TournamentProject = objectType({ + name: "TournamentProject", + definition(t) { + t.nonNull.field('project', { type: "Project" }); + t.nonNull.field('tournament', { type: "Tournament" }) + } +}); const TournamentEvent = objectType({ name: 'TournamentEvent', @@ -551,6 +557,7 @@ const updateTournamentRegistration = extendType({ module.exports = { // Types Tournament, + TournamentProject, // Enums TournamentEventTypeEnum, diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 6ea3c8b..09a77b7 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -647,6 +647,7 @@ export type StoryInputType = { }; export enum Team_Member_Role { + Owner = 'Owner', Admin = 'Admin', Maker = 'Maker' }