Merge commit 'refs/pull/151/head' of https://github.com/peakshift/makers.bolt.fun into image-upload-management

This commit is contained in:
MTG2000
2022-09-13 10:04:26 +03:00
77 changed files with 3389 additions and 1232 deletions

View File

@@ -28,6 +28,11 @@ declare global {
}
export interface NexusGenInputs {
ImageInput: { // input type
id?: string | null; // String
name?: string | null; // String
url: string; // String!
}
MakerRoleInput: { // input type
id: number; // Int!
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
@@ -36,7 +41,7 @@ export interface NexusGenInputs {
id: number; // Int!
}
ProfileDetailsInput: { // input type
avatar?: string | null; // String
avatar?: NexusGenInputs['ImageInput'] | null; // ImageInput
bio?: string | null; // String
discord?: string | null; // String
email?: string | null; // String
@@ -59,7 +64,7 @@ export interface NexusGenInputs {
}
StoryInputType: { // input type
body: string; // String!
cover_image?: string | null; // String
cover_image?: NexusGenInputs['ImageInput'] | null; // ImageInput
id?: number | null; // Int
is_published?: boolean | null; // Boolean
tags: string[]; // [String!]!
@@ -94,7 +99,6 @@ export interface NexusGenScalars {
export interface NexusGenObjects {
Author: { // root type
avatar: string; // String!
id: number; // Int!
join_date: NexusGenScalars['Date']; // Date!
lightning_address?: string | null; // String
@@ -110,7 +114,6 @@ export interface NexusGenObjects {
applicants_count: number; // Int!
applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]!
body: string; // String!
cover_image?: string | null; // String
createdAt: NexusGenScalars['Date']; // Date!
deadline: string; // String!
excerpt: string; // String!
@@ -128,7 +131,6 @@ export interface NexusGenObjects {
workplan: string; // String!
}
Category: { // root type
cover_image?: string | null; // String
icon?: string | null; // String
id: number; // Int!
title: string; // String!
@@ -153,7 +155,6 @@ export interface NexusGenObjects {
title: string; // String!
}
Hackathon: { // root type
cover_image: string; // String!
description: string; // String!
end_date: NexusGenScalars['Date']; // Date!
id: number; // Int!
@@ -180,7 +181,6 @@ export interface NexusGenObjects {
}
Mutation: {};
MyProfile: { // root type
avatar: string; // String!
bio?: string | null; // String
discord?: string | null; // String
email?: string | null; // String
@@ -212,13 +212,10 @@ export interface NexusGenObjects {
votes_count: number; // Int!
}
Project: { // root type
cover_image: string; // String!
description: string; // String!
id: number; // Int!
lightning_address?: string | null; // String
lnurl_callback_url?: string | null; // String
screenshots: string[]; // [String!]!
thumbnail_image: string; // String!
title: string; // String!
votes_count: number; // Int!
website: string; // String!
@@ -236,7 +233,6 @@ export interface NexusGenObjects {
}
Story: { // root type
body: string; // String!
cover_image?: string | null; // String
createdAt: NexusGenScalars['Date']; // Date!
excerpt: string; // String!
id: number; // Int!
@@ -253,7 +249,6 @@ export interface NexusGenObjects {
title: string; // String!
}
Tournament: { // root type
cover_image: string; // String!
description: string; // String!
end_date: NexusGenScalars['Date']; // Date!
id: number; // Int!
@@ -304,7 +299,6 @@ export interface NexusGenObjects {
projects: NexusGenRootTypes['Project'][]; // [Project!]!
}
User: { // root type
avatar: string; // String!
bio?: string | null; // String
discord?: string | null; // String
github?: string | null; // String

View File

@@ -115,6 +115,12 @@ type Hackathon {
website: String!
}
input ImageInput {
id: String
name: String
url: String!
}
type LnurlDetails {
commentAllowed: Int
maxSendable: Int
@@ -219,7 +225,7 @@ type PostComment {
}
input ProfileDetailsInput {
avatar: String
avatar: ImageInput
bio: String
discord: String
email: String
@@ -330,7 +336,7 @@ type Story implements PostBase {
input StoryInputType {
body: String!
cover_image: String
cover_image: ImageInput
id: Int
is_published: Boolean
tags: [String!]!

View File

@@ -4,7 +4,8 @@ const {
extendType,
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma')
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Category = objectType({
@@ -12,7 +13,18 @@ const Category = objectType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.string('cover_image');
t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.string('icon');

View File

@@ -6,6 +6,7 @@ const {
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
@@ -15,7 +16,18 @@ const Hackathon = objectType({
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('cover_image');
t.nonNull.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('start_date');
t.nonNull.date('end_date');
t.nonNull.string('location');

View File

@@ -1,4 +1,5 @@
const scalars = require('./_scalars')
const misc = require('./misc')
const category = require('./category')
const project = require('./project')
const vote = require('./vote')
@@ -10,6 +11,7 @@ const donation = require('./donation')
const tag = require('./tag')
module.exports = {
...misc,
...tag,
...scalars,
...category,

View File

@@ -0,0 +1,19 @@
const { objectType, extendType, inputObjectType } = require("nexus");
const { prisma } = require('../../../prisma');
const ImageInput = inputObjectType({
name: 'ImageInput',
definition(t) {
t.string('id');
t.string('name');
t.nonNull.string('url');
}
});
module.exports = {
// Types
ImageInput,
// Queries
}

View File

@@ -15,6 +15,9 @@ const { prisma } = require('../../../prisma');
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { ApolloError } = require('apollo-server-lambda');
const { marked } = require('marked');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { ImageInput } = require('./misc');
const { deleteImage } = require('../../../services/imageUpload.service');
const POST_TYPE = enumType({
@@ -37,7 +40,17 @@ const Author = objectType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
t.nonNull.string('avatar');
t.nonNull.string('avatar', {
async resolve(parent) {
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.avatar_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('join_date');
t.string('lightning_address');
@@ -71,7 +84,18 @@ const Story = objectType({
t.nonNull.string('type', {
resolve: () => t.typeName
});
t.string('cover_image');
t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.list.nonNull.field('comments', {
type: "PostComment",
resolve: (parent) => []
@@ -111,7 +135,9 @@ const StoryInputType = inputObjectType({
t.int('id');
t.nonNull.string('title');
t.nonNull.string('body');
t.string('cover_image');
t.field('cover_image', {
type: ImageInput
})
t.nonNull.list.nonNull.string('tags');
t.boolean('is_published')
}
@@ -137,7 +163,18 @@ const Bounty = objectType({
t.nonNull.string('type', {
resolve: () => 'Bounty'
});
t.string('cover_image');
t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.string('deadline');
t.nonNull.int('reward_amount');
t.nonNull.int('applicants_count');
@@ -342,6 +379,64 @@ const getPostById = extendType({
}
})
const addCoverImage = async (providerImageId) => {
const newCoverImage = await prisma.hostedImage.findFirst({
where: {
provider_image_id: providerImageId
}
})
if (!newCoverImage) throw new ApolloError("New cover image not found")
await prisma.hostedImage.update({
where: {
id: newCoverImage.id
},
data: {
is_used: true
}
})
return newCoverImage
}
const getHostedImageIdsFromBody = async (body, oldBodyImagesIds = null) => {
let bodyImageIds = []
const regex = /(?:!\[(.*?)\]\((.*?)\))/g
let match;
while((match = regex.exec(body))) {
const [,,value] = match
// Useful for old external images in case of duplicates. We need to be sure we are targeting an image from the good story.
const where = oldBodyImagesIds ? {
AND: [
{ url: value },
{ id: { in: oldBodyImagesIds } }
]
} :
{
url: value,
}
const hostedImage = await prisma.hostedImage.findFirst({
where
})
if (hostedImage) {
bodyImageIds.push(hostedImage.id)
await prisma.hostedImage.update({
where: {
id: hostedImage.id
},
data: {
is_used: true
}
})
}
}
return bodyImageIds
}
const createStory = extendType({
type: 'Mutation',
definition(t) {
@@ -358,20 +453,64 @@ const createStory = extendType({
let was_published = false;
// TODO: validate post data
let coverImage = null
let bodyImageIds = []
// Edit story
if (id) {
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true,
is_published: true
is_published: true,
cover_image_id: true,
body_image_ids: true
}
})
was_published = oldPost.is_published;
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
}
// TODO: validate post data
if (user.id !== oldPost.user_id) throw new ApolloError("Not post author")
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body, oldPost.body_image_ids)
// Old cover image is found
if (oldPost.cover_image_id) {
const oldCoverImage = await prisma.hostedImage.findFirst({
where : {
id: oldPost.cover_image_id
}
})
// New cover image
if (cover_image?.id && cover_image.id !== oldCoverImage?.provider_image_id) {
await deleteImage(oldCoverImage.id)
coverImage = await addCoverImage(cover_image.id)
} else {
coverImage = oldCoverImage
}
} else {
// No old image found and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Remove unused body images
const unusedImagesIds = oldPost.body_image_ids.filter(x => !bodyImageIds.includes(x));
unusedImagesIds.map(async i => await deleteImage(i))
} else {
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body)
// New story and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Preprocess & insert
const htmlBody = marked.parse(body);
@@ -383,6 +522,16 @@ const createStory = extendType({
.replace(/"/g, '"')
;
const coverImageRel = coverImage ? {
cover_image_rel: {
connect:
{
id: coverImage ? coverImage.id : null
}
}
} : {}
if (id) {
await prisma.story.update({
where: { id },
@@ -398,7 +547,7 @@ const createStory = extendType({
data: {
title,
body,
cover_image,
cover_image: '',
excerpt,
is_published: was_published || is_published,
tags: {
@@ -415,16 +564,17 @@ const createStory = extendType({
}
})
},
body_image_ids: bodyImageIds,
...coverImageRel
}
})
}
return prisma.story.create({
return await prisma.story.create({
data: {
title,
body,
cover_image,
cover_image: '',
excerpt,
is_published,
tags: {
@@ -445,7 +595,9 @@ const createStory = extendType({
connect: {
id: user.id,
}
}
},
body_image_ids: bodyImageIds,
...coverImageRel
}
})
}
@@ -470,17 +622,39 @@ const deleteStory = extendType({
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true
user_id: true,
body_image_ids: true,
cover_image_id: true
}
})
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
return prisma.story.delete({
const deletedPost = await prisma.story.delete({
where: {
id
}
})
const coverImage = await prisma.hostedImage.findMany({
where: {
OR: [
{ id: oldPost.cover_image_id },
{
id: {
in: oldPost.body_image_ids
}
}
]
},
select: {
id: true,
provider_image_id: true
}
})
coverImage.map(async i => await deleteImage(i.id))
return deletedPost
}
})
},

View File

@@ -6,6 +6,7 @@ const {
nonNull,
} = require('nexus')
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
const { MakerRole } = require('./users');
@@ -17,9 +18,44 @@ const Project = objectType({
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('cover_image');
t.nonNull.string('thumbnail_image');
t.nonNull.list.nonNull.string('screenshots');
t.nonNull.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.string('thumbnail_image', {
async resolve(parent) {
if (!parent.thumbnail_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.thumbnail_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.list.nonNull.string('screenshots', {
async resolve(parent) {
if (!parent.screenshots_ids) return null
const imgObject = await prisma.hostedImage.findMany({
where: {
id: { in: parent.screenshots_ids }
}
});
return imgObject.map(img => {
return resolveImgObjectToUrl(img);
});
}
});
t.nonNull.string('website');
t.string('lightning_address');
t.string('lnurl_callback_url');

View File

@@ -3,7 +3,10 @@ const { prisma } = require('../../../prisma');
const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus");
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers");
const { ImageInput } = require('./misc');
const { Tournament } = require('./tournament');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { deleteImage } = require('../../../services/imageUpload.service');
@@ -13,7 +16,18 @@ const BaseUser = interfaceType({
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
t.nonNull.string('avatar');
t.nonNull.string('avatar', {
async resolve(parent) {
if (!parent.avatar_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.avatar_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('join_date');
t.string('role');
t.string('jobTitle')
@@ -266,7 +280,9 @@ const ProfileDetailsInput = inputObjectType({
name: 'ProfileDetailsInput',
definition(t) {
t.string('name');
t.string('avatar');
t.field('avatar', {
type: ImageInput
})
t.string('email')
t.string('jobTitle')
t.string('lightning_address')
@@ -294,14 +310,48 @@ const updateProfileDetails = extendType({
throw new Error("You have to login");
// TODO: validate new data
// ----------------
// Check if the user uploaded a new image, and if so,
// remove the old one from the hosting service, then replace it with this one
// ----------------
let avatarId = user.avatar_id;
if (args.data.avatar.id) {
const newAvatarProviderId = args.data.avatar.id;
const newAvatar = await prisma.hostedImage.findFirst({
where: {
provider_image_id: newAvatarProviderId
}
})
if (newAvatar && newAvatar.id !== user.avatar_id) {
avatarId = newAvatar.id;
await prisma.hostedImage.update({
where: {
id: newAvatar.id
},
data: {
is_used: true
}
});
await deleteImage(user.avatar_id)
}
}
// Preprocess & insert
return prisma.user.update({
where: {
id: user.id,
},
data: removeNulls(args.data)
data: removeNulls({
...args.data,
avatar_id: avatarId,
//hack to remove avatar from args.data
// can be removed later with a schema data validator
avatar: '',
})
})
}
})