feat: add image management for stories and user avatar

This commit is contained in:
Dolu
2022-09-09 17:42:43 +02:00
parent 35ccec37b2
commit acdf617fb0
14 changed files with 256 additions and 54 deletions

View File

@@ -88,7 +88,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

View File

@@ -5,7 +5,7 @@ const {
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const resolveImgObjectToUrl = require('../../../utils/resolveImageUrl');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Category = objectType({
@@ -15,6 +15,7 @@ const Category = objectType({
t.nonNull.string('title');
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

View File

@@ -6,7 +6,7 @@ const {
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const resolveImgObjectToUrl = require('../../../utils/resolveImageUrl');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
@@ -18,6 +18,7 @@ const Hackathon = objectType({
t.nonNull.string('description');
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

View File

@@ -15,8 +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 { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { ImageInput } = require('./misc');
const { deleteImage } = require('../../../services/imageUpload.service');
const POST_TYPE = enumType({
@@ -39,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');
@@ -75,6 +86,7 @@ const Story = objectType({
});
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
@@ -153,6 +165,7 @@ const Bounty = objectType({
});
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
@@ -366,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) {
@@ -382,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);
@@ -408,12 +523,14 @@ const createStory = extendType({
;
// ----------------
// Check the uploaded cover image & the images in the body,
// remove the old one from the hosting service, then replace it with these ones
// ----------------
const coverImageRel = coverImage ? {
cover_image_rel: {
connect:
{
id: coverImage ? coverImage.id : null
}
}
} : {}
if (id) {
await prisma.story.update({
@@ -430,7 +547,7 @@ const createStory = extendType({
data: {
title,
body,
cover_image: cover_image.url,
cover_image: '',
excerpt,
is_published: was_published || is_published,
tags: {
@@ -447,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.url,
cover_image: '',
excerpt,
is_published,
tags: {
@@ -477,7 +595,9 @@ const createStory = extendType({
connect: {
id: user.id,
}
}
},
body_image_ids: bodyImageIds,
...coverImageRel
}
})
}
@@ -502,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,7 +6,7 @@ const {
nonNull,
} = require('nexus')
const { prisma } = require('../../../prisma');
const resolveImgObjectToUrl = require('../../../utils/resolveImageUrl');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
const { MakerRole } = require('./users');
@@ -20,6 +20,7 @@ const Project = objectType({
t.nonNull.string('description');
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
@@ -31,6 +32,7 @@ const Project = objectType({
});
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
@@ -42,6 +44,7 @@ const Project = objectType({
});
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 }

View File

@@ -6,6 +6,7 @@ const {
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
@@ -18,6 +19,7 @@ const Tournament = objectType({
t.nonNull.string('thumbnail_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

View File

@@ -5,7 +5,7 @@ const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers");
const { ImageInput } = require('./misc');
const { Tournament } = require('./tournaments');
const resolveImgObjectToUrl = require('../../../utils/resolveImageUrl');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { deleteImage } = require('../../../services/imageUpload.service');
@@ -18,6 +18,7 @@ const BaseUser = interfaceType({
t.nonNull.string('name');
t.nonNull.string('avatar', {
async resolve(parent) {
if (!parent.avatar_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.avatar_id
@@ -301,15 +302,6 @@ const updateProfileDetails = extendType({
if (newAvatar && newAvatar.id !== user.avatar_id) {
avatarId = newAvatar.id;
// Set is_used to false in case of deleteImage() fail. The scheduled job will try to delete the HostedImage row
await prisma.hostedImage.update({
where: {
id: user.avatar_id
},
data: {
is_used: false
}
});
await prisma.hostedImage.update({
where: {
id: newAvatar.id
@@ -319,7 +311,7 @@ const updateProfileDetails = extendType({
}
});
deleteImage(user.avatar_id)
await deleteImage(user.avatar_id)
}
}

View File

@@ -85,11 +85,21 @@ const loginHandler = async (req, res) => {
const nostr_prv_key = generatePrivateKey();
const nostr_pub_key = getPublicKey(nostr_prv_key);
const avatar = await prisma.hostedImage.create({
data: {
filename: 'avatar.svg',
provider: 'external',
is_used: true,
url: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
provider_image_id: ''
}
})
const createdUser = await prisma.user.create({
data: {
pubKey: key,
name: key,
avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
avatar_id: avatar.id,
nostr_prv_key,
nostr_pub_key,
},

View File

@@ -5,6 +5,7 @@ const extractKeyFromCookie = require('../../utils/extractKeyFromCookie')
const { getUserByPubKey } = require('../../auth/utils/helperFuncs')
const { getDirectUploadUrl } = require('../../services/imageUpload.service')
const { prisma } = require('../../prisma')
const { getUrlFromProvider } = require('../../utils/resolveImageUrl')
const postUploadImageUrl = async (req, res) => {
@@ -23,7 +24,7 @@ const postUploadImageUrl = async (req, res) => {
const hostedImage = await prisma.hostedImage.create({
data: {
filename,
url: uploadUrl.uploadURL,
url: getUrlFromProvider(uploadUrl.provider, uploadUrl.id),
provider_image_id: uploadUrl.id,
provider: uploadUrl.provider
},

View File

@@ -42,6 +42,19 @@ async function getDirectUploadUrl() {
return { id: data.id, uploadURL: data.uploadURL, provider: 'cloudflare' }
}
async function deleteImageFromProvider(providerImageId) {
try {
const url = operationUrls['image.delete'] + providerImageId
const result = await axios.delete(url, getAxiosConfig())
if (!result.data.success) {
throw new Error(result.data, { cause: result.data.errors })
}
} catch (error) {
throw error
}
}
async function deleteImage(hostedImageId) {
if (!hostedImageId) throw new Error("argument 'hostedImageId' must be provider")
@@ -52,23 +65,34 @@ async function deleteImage(hostedImageId) {
})
if (!hostedImage) throw new Error(`No HostedImage row found for HostedImage.id=${hostedImageId}`)
if (hostedImage.provider_image_id && hostedImage.provider_image_id === '') throw new Error(`Field 'provider_image_id' for HostedImage.id=${hostedImageId} must not be empty. Current value '${hostedImage.provider_image_id}'`)
if (hostedImage.provider_image_id && hostedImage.provider_image_id === '')
throw new Error(`Field 'provider_image_id' for HostedImage.id=${hostedImageId} must not be empty. Current value '${hostedImage.provider_image_id}'`)
const url = operationUrls['image.delete'] + hostedImage.provider_image_id
const result = await axios.delete(url, getAxiosConfig())
if (!result.data.success) {
throw new Error(result.data, { cause: result.data.errors })
}
// Set is_used to false in case of deletion fail from the hosting image provider. The scheduled job will try to delete the HostedImage row
await prisma.hostedImage.update({
where: {
id: hostedImage.id,
},
data: {
is_used: false,
},
})
if (hostedImage.provider_image_id && hostedImage.provider_image_id !== '') {
deleteImageFromProvider(hostedImage.provider_image_id)
.then(async () => {
await prisma.hostedImage.delete({
where: {
id: hostedImageId,
},
})
})
.catch((error) => console.error(error))
}
}
module.exports = {
getDirectUploadUrl,
deleteImage,
deleteImageFromProvider,
}

View File

@@ -26,16 +26,20 @@ function resolveImgObjectToUrl(imgObject, variant = null) {
return imgObject.url
}
const provider = PROVIDERS.find((p) => p.name === imgObject.provider)
return getUrlFromProvider(imgObject.provider, imgObject.provider_image_id, variant)
}
if (provider) {
if (provider && provider.name === 'cloudflare') {
const variantName = variant ?? provider.variants.find((v) => v.default).name
return provider.prefixUrl + imgObject.provider_image_id + '/' + variantName
function getUrlFromProvider(provider, providerImageId, variant = null) {
const p = PROVIDERS.find((p) => p.name === provider)
if (p) {
if (p && p.name === 'cloudflare') {
const variantName = variant ?? p.variants.find((v) => v.default).name
return p.prefixUrl + providerImageId + '/' + variantName
}
}
throw new Error('Hosting images provider not supported')
}
module.exports = resolveImgObjectToUrl
module.exports = { resolveImgObjectToUrl, getUrlFromProvider }

View File

@@ -13,7 +13,8 @@ ADD COLUMN "screenshots_ids" INTEGER[],
ADD COLUMN "thumbnail_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "cover_image_id" INTEGER;
ALTER TABLE "Story" ADD COLUMN "body_image_ids" INTEGER[],
ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "avatar_id" INTEGER;

View File

@@ -185,6 +185,7 @@ model Story {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
body String
body_image_ids Int[]
excerpt String
cover_image String?
cover_image_id Int?

View File

@@ -180,9 +180,30 @@ async function migrateOldImages() {
select: {
id: true,
cover_image: true,
body: true,
}
})
for (const story of stories) {
/**
* Story.body to Story.body_image_ids
**/
let bodyImageIds = [];
const regex = /(?:!\[(.*?)\]\((.*?)\))/g
let match;
while((match = regex.exec(story.body))) {
const [,,value] = match
let hostedImageId = await _insertInHostedImage(value)
bodyImageIds.push(hostedImageId)
}
if (bodyImageIds.length > 0) {
await _updateObjectWithHostedImageId(prisma.story, story.id, {
body_image_ids: bodyImageIds,
})
}
/**
* Story.cover_image to Story.cover_image_id
**/
if (story.cover_image) {
let hostedImageId = await _insertInHostedImage(story.cover_image)
await _updateObjectWithHostedImageId(prisma.story, story.id, {