mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-17 22:34:21 +01:00
feat: add image management for stories and user avatar
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
await prisma.hostedImage.delete({
|
||||
// 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: hostedImageId,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
@@ -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?
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user