From 1044a3aa89c03531b3ecdd23bf96356f4849d1b1 Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Feb 2025 00:07:47 -0800 Subject: [PATCH 01/10] ctrl c + ctrl v == SuperLebron (Luka) --- .../cloudinary/deleteCloudinaryItem.ts | 32 ++++++++ .../cloudinary/generateCloudinarySignature.ts | 32 ++++++++ app/(api)/_actions/media/createMediaItem.ts | 10 +++ app/(api)/_actions/media/deleteMediaItem.ts | 10 +++ app/(api)/_actions/media/findMediaItem.ts | 11 +++ app/(api)/_actions/media/updateMediaItems.ts | 10 +++ app/(api)/_types/media/MediaItem.ts | 16 ++++ app/(api)/_utils/callback/withCallback.ts | 10 +++ app/(pages)/_utils/converFileToMediaItem.ts | 15 ++++ app/(pages)/_utils/uploadMediaItem.ts | 76 +++++++++++++++++++ app/_utils/callbacks.ts | 13 ++++ 11 files changed, 235 insertions(+) create mode 100644 app/(api)/_actions/cloudinary/deleteCloudinaryItem.ts create mode 100644 app/(api)/_actions/cloudinary/generateCloudinarySignature.ts create mode 100644 app/(api)/_actions/media/createMediaItem.ts create mode 100644 app/(api)/_actions/media/deleteMediaItem.ts create mode 100644 app/(api)/_actions/media/findMediaItem.ts create mode 100644 app/(api)/_actions/media/updateMediaItems.ts create mode 100644 app/(api)/_types/media/MediaItem.ts create mode 100644 app/(api)/_utils/callback/withCallback.ts create mode 100644 app/(pages)/_utils/converFileToMediaItem.ts create mode 100644 app/(pages)/_utils/uploadMediaItem.ts create mode 100644 app/_utils/callbacks.ts diff --git a/app/(api)/_actions/cloudinary/deleteCloudinaryItem.ts b/app/(api)/_actions/cloudinary/deleteCloudinaryItem.ts new file mode 100644 index 0000000..ec624ab --- /dev/null +++ b/app/(api)/_actions/cloudinary/deleteCloudinaryItem.ts @@ -0,0 +1,32 @@ +'use server'; +import HttpError from '@utils/response/HttpError'; +import { v2 as cloudinary } from 'cloudinary'; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +export default async function DeleteCloudinaryObject( + cloudinary_id: string, + type: string +) { + try { + const result = await cloudinary.uploader.destroy(cloudinary_id, { + resource_type: type, + }); + return { + ok: true, + body: result, + error: null, + }; + } catch (e) { + const err = e as HttpError; + return { + ok: false, + body: null, + error: err.message, + }; + } +} diff --git a/app/(api)/_actions/cloudinary/generateCloudinarySignature.ts b/app/(api)/_actions/cloudinary/generateCloudinarySignature.ts new file mode 100644 index 0000000..cd47177 --- /dev/null +++ b/app/(api)/_actions/cloudinary/generateCloudinarySignature.ts @@ -0,0 +1,32 @@ +'use server'; +import HttpError from '@utils/response/HttpError'; +import { v2 as cloudinary } from 'cloudinary'; + +export default async function GenerateCloudinarySignature( + requestParams: object +) { + try { + const timestamp = Math.round(new Date().getTime() / 1000); + const signature = cloudinary.utils.api_sign_request( + { + timestamp, + ...requestParams, + }, + process.env.CLOUDINARY_API_SECRET as string + ); + + const body = { + cloudUrl: `https://api.cloudinary.com/v1_1/${process.env.CLOUDINARY_NAME}`, + requestParams: { + api_key: process.env.CLOUDINARY_API_KEY, + timestamp, + signature, + ...requestParams, + }, + }; + return { ok: true, body, error: null }; + } catch (e) { + const error = e as HttpError; + return { ok: false, body: null, error: error.message }; + } +} diff --git a/app/(api)/_actions/media/createMediaItem.ts b/app/(api)/_actions/media/createMediaItem.ts new file mode 100644 index 0000000..b8b1d64 --- /dev/null +++ b/app/(api)/_actions/media/createMediaItem.ts @@ -0,0 +1,10 @@ +'use server'; +import { createMediaItem } from '@datalib/media/createMediaItem'; +import { revalidatePath } from 'next/cache'; +import WithCallback from '@app/(api)/_utils/callback/withCallback'; + +export const CreateMediaItem = WithCallback(async (body: object) => { + const createMediaItemRes = await createMediaItem(body); + revalidatePath('/uploaded-media'); + return createMediaItemRes; +}); diff --git a/app/(api)/_actions/media/deleteMediaItem.ts b/app/(api)/_actions/media/deleteMediaItem.ts new file mode 100644 index 0000000..55eeccb --- /dev/null +++ b/app/(api)/_actions/media/deleteMediaItem.ts @@ -0,0 +1,10 @@ +'use server'; +import { deleteMediaItem } from '@datalib/media/deleteMediaItem'; +import { revalidatePath } from 'next/cache'; +import WithCallback from '@app/(api)/_utils/callback/withCallback'; + +export const DeleteMediaItem = WithCallback(async (id: string) => { + const deleteMediaRes = await deleteMediaItem(id); + revalidatePath('/uploaded-media'); + return deleteMediaRes; +}); diff --git a/app/(api)/_actions/media/findMediaItem.ts b/app/(api)/_actions/media/findMediaItem.ts new file mode 100644 index 0000000..82c9952 --- /dev/null +++ b/app/(api)/_actions/media/findMediaItem.ts @@ -0,0 +1,11 @@ +'use server'; + +import { findMediaItem, findMediaItems } from '@datalib/media/findMediaItem'; + +export async function FindMediaItem(id: string) { + return findMediaItem(id); +} + +export async function FindMediaItems(query: object = {}) { + return findMediaItems(query); +} diff --git a/app/(api)/_actions/media/updateMediaItems.ts b/app/(api)/_actions/media/updateMediaItems.ts new file mode 100644 index 0000000..1fb8233 --- /dev/null +++ b/app/(api)/_actions/media/updateMediaItems.ts @@ -0,0 +1,10 @@ +'use server'; +import { createMediaItem } from '@datalib/media/createMediaItem'; +import { revalidatePath } from 'next/cache'; +import WithCallback from '@app/(api)/_utils/callback/withCallback'; +// import WithCallback from '@utils/callback/withCallback'; +export const CreateMediaItem = WithCallback(async (body: object) => { + const createMediaItemRes = await createMediaItem(body); + revalidatePath('/uploaded-media'); + return createMediaItemRes; +}); \ No newline at end of file diff --git a/app/(api)/_types/media/MediaItem.ts b/app/(api)/_types/media/MediaItem.ts new file mode 100644 index 0000000..d43e6dc --- /dev/null +++ b/app/(api)/_types/media/MediaItem.ts @@ -0,0 +1,16 @@ +interface MediaItem { + _id: string | null; + cloudinary_id: string | null; + name: string; + type: string; + format: string; + src: string; + alt?: string; + size: number; + width: number | null; + height: number | null; + _created_at?: string | null; + _last_modified?: string | null; +} + +export default MediaItem; diff --git a/app/(api)/_utils/callback/withCallback.ts b/app/(api)/_utils/callback/withCallback.ts new file mode 100644 index 0000000..da1e7a2 --- /dev/null +++ b/app/(api)/_utils/callback/withCallback.ts @@ -0,0 +1,10 @@ +import callbacks from '@app/_utils/callbacks'; + +type CallbackFunction = (...args: any[]) => any; +export default function WithCallback(func: (..._: any) => any) { + return async (...args: any) => { + const res = await func(...args); + callbacks.onUpdate(); + return res; + }; +} \ No newline at end of file diff --git a/app/(pages)/_utils/converFileToMediaItem.ts b/app/(pages)/_utils/converFileToMediaItem.ts new file mode 100644 index 0000000..8ee74dd --- /dev/null +++ b/app/(pages)/_utils/converFileToMediaItem.ts @@ -0,0 +1,15 @@ +import MediaItem from '@typeDefs/media/MediaItem'; +export default function convertFileToMediaItem(file: File): MediaItem { + const [fileType, fileFormat] = file.type.split('/'); + return { + _id: null, + cloudinary_id: null, + name: file.name, + type: fileType, + format: fileFormat, + src: URL.createObjectURL(file), + size: file.size, + width: null, + height: null, + }; +} diff --git a/app/(pages)/_utils/uploadMediaItem.ts b/app/(pages)/_utils/uploadMediaItem.ts new file mode 100644 index 0000000..a452e6e --- /dev/null +++ b/app/(pages)/_utils/uploadMediaItem.ts @@ -0,0 +1,76 @@ +import GenerateCloudinarySignature from '@actions/cloudinary/generateCloudinarySignature'; +import { CreateMediaItem } from '@actions/media/createMediaItem'; +import HttpError from '@utils/response/HttpError'; +import MediaItem from '@typeDefs/media/MediaItem'; +import DeleteCloudinaryObject from '@actions/cloudinary/deleteCloudinaryItem'; + +export default async function uploadMediaItem(mediaItem: MediaItem) { + try { + const cloudinaryType = getCloudinaryType(mediaItem.type); + const fileRes = await fetch(mediaItem.src); + const file = await fileRes.blob(); + + const requestParams = {}; + const signatureRes = await GenerateCloudinarySignature(requestParams); + if (!signatureRes.ok) { + throw new HttpError(signatureRes.error || 'Internal Server Error'); + } + + const uploadBody = new FormData(); + Object.entries(signatureRes.body?.requestParams || {}).forEach( + ([key, value]) => { + uploadBody.append(key, (value || '').toString()); + } + ); + + uploadBody.append('file', file); + const uploadRes = await fetch( + `${signatureRes.body?.cloudUrl}/${cloudinaryType}/upload`, + { + method: 'POST', + body: uploadBody, + } + ); + + const uploadData = await uploadRes.json(); + + if (uploadData.error) { + throw new HttpError(uploadData.error.message); + } + + const updatedMediaItem = { + ...mediaItem, + cloudinary_id: uploadData.public_id, + src: uploadData.secure_url, + height: uploadData.height || null, + width: uploadData.width || null, + }; + + const { _id: _, ...creationBody } = updatedMediaItem; + + const creationRes = await CreateMediaItem(creationBody); + if (!creationRes.ok) { + const deleteStatus = await DeleteCloudinaryObject( + updatedMediaItem.cloudinary_id, + getCloudinaryType(updatedMediaItem.type) + ); + const errorMsg = `${creationRes.error}${ + deleteStatus.ok ? '' : `\n${deleteStatus.error}` + }`; + throw new Error(errorMsg); + } + + return creationRes; + } catch (e) { + const err = e as HttpError; + return { ok: false, body: null, error: err.message }; + } +} + +function getCloudinaryType(type: string) { + if (type === 'video') { + return type; + } else { + return 'image'; + } +} diff --git a/app/_utils/callbacks.ts b/app/_utils/callbacks.ts new file mode 100644 index 0000000..0b39985 --- /dev/null +++ b/app/_utils/callbacks.ts @@ -0,0 +1,13 @@ +let callbacks; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callbacksModule = require('../../build-assets/callbacks.js'); + callbacks = callbacksModule.default; +} catch (e) { + callbacks = { + onUpdate: () => {}, + }; +} +export default callbacks as { + onUpdate: () => void; +}; From fad911f51e2a1354df22615a8d5a7bcf7ccbe510 Mon Sep 17 00:00:00 2001 From: kiranjoji Date: Thu, 20 Feb 2025 16:36:02 -0800 Subject: [PATCH 02/10] more dependencies --- app/(api)/_datalib/media/createMediaItem.ts | 38 +++++ app/(api)/_datalib/media/deleteMediaItem.ts | 85 +++++++++++ app/(api)/_datalib/media/findMediaItem.ts | 42 +++++ app/(api)/_datalib/media/updateMediaItems.ts | 42 +++++ app/(api)/_utils/callback/withCallback.ts | 2 +- app/(api)/_utils/mongodb/mongoClient.mjs | 18 +++ app/(api)/_utils/request/getQueries.ts | 42 +++++ app/(api)/_utils/request/isBodyEmpty.ts | 3 + app/(api)/_utils/request/parseAndReplace.js | 115 ++++++++++++++ app/(api)/_utils/request/prependAttributes.js | 71 +++++++++ app/(api)/_utils/response/DuplicateError.ts | 9 ++ app/(api)/_utils/response/Errors.ts | 15 ++ app/(api)/_utils/response/ForbiddenError.ts | 9 ++ app/(api)/_utils/response/HttpError.ts | 8 + app/(api)/_utils/response/NoContentError.ts | 9 ++ .../_utils/response/NotAuthenticatedError.ts | 9 ++ app/(api)/_utils/response/NotFoundError.ts | 9 ++ ...MediaItem.ts => convertFileToMediaItem.ts} | 0 app/(pages)/_utils/uploadMediaItem.ts | 5 +- app/_types/auth/AuthToken.ts | 6 + app/_types/auth/User.ts | 5 + app/_types/auth/UserCredentials.ts | 4 + app/_types/content/BaseContentItem.ts | 9 ++ app/_types/media/MediaItem.ts | 16 ++ app/_types/response/response.ts | 5 + app/_utils/schema.ts | 12 ++ app/_utils/settings.ts | 11 ++ dist/index.d.ts | 6 + dist/index.js | 9 ++ dist/schema/ContentSchema.d.ts | 23 +++ dist/schema/ContentSchema.js | 47 ++++++ dist/schema/ContentType.d.ts | 54 +++++++ dist/schema/ContentType.js | 143 ++++++++++++++++++ package-lock.json | 23 +++ package.json | 1 + 35 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 app/(api)/_datalib/media/createMediaItem.ts create mode 100644 app/(api)/_datalib/media/deleteMediaItem.ts create mode 100644 app/(api)/_datalib/media/findMediaItem.ts create mode 100644 app/(api)/_datalib/media/updateMediaItems.ts create mode 100644 app/(api)/_utils/mongodb/mongoClient.mjs create mode 100644 app/(api)/_utils/request/getQueries.ts create mode 100644 app/(api)/_utils/request/isBodyEmpty.ts create mode 100644 app/(api)/_utils/request/parseAndReplace.js create mode 100644 app/(api)/_utils/request/prependAttributes.js create mode 100644 app/(api)/_utils/response/DuplicateError.ts create mode 100644 app/(api)/_utils/response/Errors.ts create mode 100644 app/(api)/_utils/response/ForbiddenError.ts create mode 100644 app/(api)/_utils/response/HttpError.ts create mode 100644 app/(api)/_utils/response/NoContentError.ts create mode 100644 app/(api)/_utils/response/NotAuthenticatedError.ts create mode 100644 app/(api)/_utils/response/NotFoundError.ts rename app/(pages)/_utils/{converFileToMediaItem.ts => convertFileToMediaItem.ts} (100%) create mode 100644 app/_types/auth/AuthToken.ts create mode 100644 app/_types/auth/User.ts create mode 100644 app/_types/auth/UserCredentials.ts create mode 100644 app/_types/content/BaseContentItem.ts create mode 100644 app/_types/media/MediaItem.ts create mode 100644 app/_types/response/response.ts create mode 100644 app/_utils/schema.ts create mode 100644 app/_utils/settings.ts create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/schema/ContentSchema.d.ts create mode 100644 dist/schema/ContentSchema.js create mode 100644 dist/schema/ContentType.d.ts create mode 100644 dist/schema/ContentType.js diff --git a/app/(api)/_datalib/media/createMediaItem.ts b/app/(api)/_datalib/media/createMediaItem.ts new file mode 100644 index 0000000..b4e0da8 --- /dev/null +++ b/app/(api)/_datalib/media/createMediaItem.ts @@ -0,0 +1,38 @@ +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import parseAndReplace from '@utils/request/parseAndReplace'; +import { HttpError, NoContentError } from '@utils/response/Errors'; +import isBodyEmpty from '@utils/request/isBodyEmpty'; + +export async function createMediaItem(body: object) { + try { + if (isBodyEmpty(body)) { + throw new NoContentError(); + } + const parsedBody = await parseAndReplace(body); + + const db = await getDatabase(); + const currentDate = new Date().toISOString(); + const creationStatus = await db.collection('media').insertOne({ + ...parsedBody, + _last_modified: currentDate, + _created_at: currentDate, + }); + + const createdMedia = await db.collection('media').findOne({ + _id: creationStatus.insertedId, + }); + + if (!createdMedia) { + throw new HttpError('Failed to fetch the created item'); + } + + return { ok: true, body: createdMedia, error: null }; + } catch (e) { + const error = e as HttpError; + return { + ok: false, + body: null, + error: error.message || 'Internal Server Error', + }; + } +} diff --git a/app/(api)/_datalib/media/deleteMediaItem.ts b/app/(api)/_datalib/media/deleteMediaItem.ts new file mode 100644 index 0000000..97588d4 --- /dev/null +++ b/app/(api)/_datalib/media/deleteMediaItem.ts @@ -0,0 +1,85 @@ +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import { HttpError, NotFoundError } from '@utils/response/Errors'; +import { v2 as cloudinary } from 'cloudinary'; +import schema from '@app/_utils/schema'; +import { FieldType, Field } from '@dist/index'; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +export async function deleteMediaItem(id: string) { + try { + const db = await getDatabase(); + + const objectId = ObjectId.createFromHexString(id); + const mediaItem = await db.collection('media').findOne({ + _id: objectId, + }); + + await cloudinary.uploader.destroy(mediaItem.cloudinary_id, { + resource_type: mediaItem.format === 'pdf' ? 'image' : mediaItem.type, + }); + + const deleteStatus = await db.collection('media').deleteOne({ + _id: objectId, + }); + + const content_types = schema.getNames(); + await Promise.all( + content_types.map((content_type: string) => { + const contentSchema = schema.get(content_type); + if (!contentSchema) { + throw new NotFoundError( + `Content type: ${content_type} does not exist.` + ); + } + const mediaFields = contentSchema + .getFieldArray() + .filter((field: Field) => field.type === FieldType.MEDIA_LIST) + .map((field: Field) => field.name); + + const updatePullList: { [key: string]: any } = {}; + mediaFields.forEach((field: string) => { + updatePullList[field] = objectId; + }); + + const mediaFieldQueries = mediaFields.map((field: string) => ({ + [field]: objectId, + })); + if (mediaFieldQueries.length === 0) { + return null; + } + return db.collection(content_type).updateMany( + { + $or: mediaFieldQueries, + }, + { + $pull: updatePullList, + } + ); + }) + ); + + if (deleteStatus.deletedCount === 0) { + throw new NotFoundError(`media item with id: ${id} not found.`); + } + + return { + ok: true, + body: 'media item deleted.', + error: null, + }; + } catch (error) { + const e = error as HttpError; + return { + ok: false, + body: null, + error: e.message || 'Internal Server Error', + }; + } +} diff --git a/app/(api)/_datalib/media/findMediaItem.ts b/app/(api)/_datalib/media/findMediaItem.ts new file mode 100644 index 0000000..ec26430 --- /dev/null +++ b/app/(api)/_datalib/media/findMediaItem.ts @@ -0,0 +1,42 @@ +import { ObjectId } from 'mongodb'; +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import { HttpError, NotFoundError } from '@utils/response/Errors'; + +export async function findMediaItem(id: string) { + try { + const db = await getDatabase(); + const objectId = ObjectId.createFromHexString(id); + + const mediaItem = await db.collection('media').findOne({ + _id: objectId, + }); + + if (!mediaItem) { + throw new NotFoundError(`Media item with id: ${id} not found.`); + } + + return { ok: true, body: mediaItem, error: null }; + } catch (error) { + const e = error as HttpError; + return { + ok: false, + body: null, + error: e.message || 'Internal Server Error', + }; + } +} + +export async function findMediaItems(query: object = {}) { + try { + const db = await getDatabase(); + const mediaItems = await db.collection('media').find(query).toArray(); + return { ok: true, body: mediaItems, error: null }; + } catch (error) { + const e = error as HttpError; + return { + ok: false, + body: null, + error: e.message || 'Internal Server Error', + }; + } +} diff --git a/app/(api)/_datalib/media/updateMediaItems.ts b/app/(api)/_datalib/media/updateMediaItems.ts new file mode 100644 index 0000000..55d4ba0 --- /dev/null +++ b/app/(api)/_datalib/media/updateMediaItems.ts @@ -0,0 +1,42 @@ +import { ObjectId } from 'mongodb'; +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import parseAndReplace from '@utils/request/parseAndReplace'; +import { + HttpError, + NoContentError, + NotFoundError, +} from '@utils/response/Errors'; + +export async function updateMediaItem(id: string, body = {}) { + try { + if (!body || Object.keys(body).length === 0) { + throw new NoContentError(); + } + + const db = await getDatabase(); + + const objectId = ObjectId.createFromHexString(id); + const updates = await parseAndReplace(body); + updates.$set._last_modified = new Date().toISOString(); + + const updateStatus = await db.collection('media').updateOne( + { + _id: objectId, + }, + updates + ); + + if (updateStatus === null) { + throw new NotFoundError(`Judge with id: ${id} not found.`); + } + + return { ok: true, body: updateStatus, error: null }; + } catch (error) { + const e = error as HttpError; + return { + ok: false, + body: null, + error: e.message || 'Internal Server Error', + }; + } +} diff --git a/app/(api)/_utils/callback/withCallback.ts b/app/(api)/_utils/callback/withCallback.ts index da1e7a2..6330532 100644 --- a/app/(api)/_utils/callback/withCallback.ts +++ b/app/(api)/_utils/callback/withCallback.ts @@ -7,4 +7,4 @@ export default function WithCallback(func: (..._: any) => any) { callbacks.onUpdate(); return res; }; -} \ No newline at end of file +} diff --git a/app/(api)/_utils/mongodb/mongoClient.mjs b/app/(api)/_utils/mongodb/mongoClient.mjs new file mode 100644 index 0000000..aeccbf7 --- /dev/null +++ b/app/(api)/_utils/mongodb/mongoClient.mjs @@ -0,0 +1,18 @@ +import { MongoClient } from 'mongodb'; + +const uri = process.env.MONGO_CONNECTION_STRING; +let cachedClient = null; + +export async function getClient() { + if (cachedClient) { + return cachedClient; + } + const client = new MongoClient(uri); + cachedClient = client; + return cachedClient; +} + +export async function getDatabase() { + const client = await getClient(); + return client.db(); +} diff --git a/app/(api)/_utils/request/getQueries.ts b/app/(api)/_utils/request/getQueries.ts new file mode 100644 index 0000000..4f8e254 --- /dev/null +++ b/app/(api)/_utils/request/getQueries.ts @@ -0,0 +1,42 @@ +import { NextRequest } from 'next/server'; +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import { ObjectId } from 'mongodb'; + +function typeCast(value: string, type: string) { + switch (type) { + case 'int': + return isNaN(+value) ? value : +value; + case 'objectId': + try { + return ObjectId.createFromHexString(value); + } catch { + return value; + } + case 'bool': + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } else { + return value; + } + default: + return value; + } +} + +export default async function getQueries( + request: NextRequest, + content_type: string +) { + const db = await getDatabase(); + const query_entries = request.nextUrl.searchParams.entries(); + const schema = (await db.listCollections({ name: content_type }).toArray())[0] + .options.validator; + + const output: { [key: string]: any } = {}; + for (const [key, val] of query_entries) { + output[key] = typeCast(val, schema.$jsonSchema.properties[key]?.bsonType); + } + return output; +} diff --git a/app/(api)/_utils/request/isBodyEmpty.ts b/app/(api)/_utils/request/isBodyEmpty.ts new file mode 100644 index 0000000..2519abb --- /dev/null +++ b/app/(api)/_utils/request/isBodyEmpty.ts @@ -0,0 +1,3 @@ +export default function isBodyEmpty(obj: object) { + return Object.keys(obj).length === 0; +} diff --git a/app/(api)/_utils/request/parseAndReplace.js b/app/(api)/_utils/request/parseAndReplace.js new file mode 100644 index 0000000..5dc1b66 --- /dev/null +++ b/app/(api)/_utils/request/parseAndReplace.js @@ -0,0 +1,115 @@ +import { ObjectId } from 'mongodb'; + +import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +/** + * Takes object resembling example below with an "*expandIds" field: + * { + * "*expandIds": { + * "ids": ["658f94018dac260ae7b17fce", "658f940e8dac260ae7b17fd0"], + * "from": "pokemon" + * } + * } + * When an object that resembles the above object is encountered, return an array of documents + * from the "from" collection + */ +async function expandIds(obj) { + obj = obj['*expandIds']; + const db = await getDatabase(); + const documents = await db + .collection(obj.from) + .find({ + _id: { $in: obj.ids.map((id) => ObjectId.createFromHexString(id)) }, + }) + .toArray(); + return documents; +} + +/** + * Takes object resembling example below with an "*expandId" field: + * { + * "*expandId": { + * "id": "658f94018dac260ae7b17fce", + * "from": "pokemon" + * } + * } + * When an object that resembles the above object is encountered, return a document + * from the "from" collection + */ +async function expandId(obj) { + obj = obj['*expandId']; + const db = await getDatabase(); + const documents = await db.collection(obj.from).findOne({ + _id: ObjectId.createFromHexString(obj.id), + }); + return documents; +} + +/** + * Takes object resembling example below with a "*convertIds" field: + * { + * "*convertIds": { + * "ids": ["658f94018dac260ae7b17fce", "658f940e8dac260ae7b17fd0"], + * } + * } + * + * Returns the array of ids converted to ObjectIds + */ +async function convertIds(obj) { + obj = obj['*convertIds']; + return obj.ids.map((id) => ObjectId.createFromHexString(id)); +} + +/** + * Takes object resembling example below with a "*convertId" field: + * { + * "*convertId": { + * "id": "658f94018dac260ae7b17fce", + * } + * } + * + * Returns the id converted to an ObjectId + */ +async function convertId(obj) { + obj = obj['*convertId']; + return ObjectId.createFromHexString(obj.id); +} + +/** + * Searches through a json object and replaces all objects that have a key of + * "*keyword" with the object processed with replaceFunc() + * + * replacements comes in the form of: + * { + * "*keyword": replaceFunc, + * "*keyword2": replaceFunc2 + * } + */ +async function searchAndReplace(obj, replacements) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + for (const [key, val] of Object.entries(obj)) { + let replaced = false; + for (const [keyword, replaceFunc] of Object.entries(replacements)) { + if (val?.[keyword] !== undefined) { + obj[key] = await replaceFunc(val); + replaced = true; + break; + } + } + if (!replaced) { + obj[key] = await searchAndReplace(val, replacements); + } + } + return obj; +} + +export default async function parseAndReplace(obj) { + const res = await searchAndReplace(obj, { + '*expandId': expandId, + '*expandIds': expandIds, + '*convertId': convertId, + '*convertIds': convertIds, + }); + return res; +} diff --git a/app/(api)/_utils/request/prependAttributes.js b/app/(api)/_utils/request/prependAttributes.js new file mode 100644 index 0000000..d93dbd0 --- /dev/null +++ b/app/(api)/_utils/request/prependAttributes.js @@ -0,0 +1,71 @@ +/** + * Takes in a json object and renames one attribute. + */ +export function renameAttribute(jsonObj, oldFieldName, newFieldName) { + if (jsonObj[oldFieldName]) { + // Create a new object with the renamed attribute + const renamedObject = { + ...jsonObj, + [newFieldName]: jsonObj[oldFieldName], + }; + + // Delete the old attribute + delete renamedObject[oldFieldName]; + return renamedObject; + } + + // Return the original object if the old attribute doesn't exist + return jsonObj; +} + +/** + * subset of renaming attribute, appends a prefix of "prefix" before the attribute name + * "prefix" comes from mongodb nested documents + */ +export function prependToAttribute(jsonObj, attribute, prefix) { + return renameAttribute(jsonObj, attribute, `${prefix}${attribute}`); +} + +/** + * For each attribute within an updater such as: + * { + * "$set": {"name": "austin", "age": 20} + * } + * (in this case, the fields in question are "name" and "age" and the updater is "$set") + * Append the prefix of prefix to each attribute. + * + * This results in: + * { + * "$set": {"PREFIXname": "austin", "PREFIXage": 20}, + * } + * where prefix = PREFIX + */ +export function prependObjectAttributes(jsonObj, prefix) { + for (const key of Object.keys(jsonObj)) { + jsonObj = prependToAttribute(jsonObj, key, prefix); + } + return jsonObj; +} + +/** + * For each updater within a request body + * { + * "$set": {"name": "austin", "age": 20}, + * "$push": {"pokemon": ..., ...} + * } + * (in this case, the updaters in question are "$set" and "$push" + * run prependObjectAttributes() on the content within + * + * This results in: + * { + * "$set": {"PREFIXname": "austin", "PREFIXage": 20}, + * "$push": {"PREFIXpokemon": ..., ...} + * } + * where prefix = PREFIX + */ +export function prependAllAttributes(jsonObj, prefix) { + for (const [key, val] of Object.entries(jsonObj)) { + jsonObj[key] = prependObjectAttributes(val, prefix); + } + return jsonObj; +} diff --git a/app/(api)/_utils/response/DuplicateError.ts b/app/(api)/_utils/response/DuplicateError.ts new file mode 100644 index 0000000..df40095 --- /dev/null +++ b/app/(api)/_utils/response/DuplicateError.ts @@ -0,0 +1,9 @@ +import HttpError from './HttpError'; + +export default class DuplicateError extends HttpError { + constructor(message: string) { + super(message); + this.name = 'DuplicateError'; + this.status = 409; + } +} \ No newline at end of file diff --git a/app/(api)/_utils/response/Errors.ts b/app/(api)/_utils/response/Errors.ts new file mode 100644 index 0000000..e3557b6 --- /dev/null +++ b/app/(api)/_utils/response/Errors.ts @@ -0,0 +1,15 @@ +import HttpError from './HttpError'; +import NoContentError from './NoContentError'; +import NotFoundError from './NotFoundError'; +import NotAuthenticatedError from './NotAuthenticatedError'; +import ForbiddenError from './ForbiddenError'; +import DuplicateError from './DuplicateError'; + +export { + HttpError, + NoContentError, + NotFoundError, + NotAuthenticatedError, + ForbiddenError, + DuplicateError, +}; \ No newline at end of file diff --git a/app/(api)/_utils/response/ForbiddenError.ts b/app/(api)/_utils/response/ForbiddenError.ts new file mode 100644 index 0000000..2f1bf4b --- /dev/null +++ b/app/(api)/_utils/response/ForbiddenError.ts @@ -0,0 +1,9 @@ +import HttpError from './HttpError'; + +export default class ForbiddenError extends HttpError { + constructor(message: string) { + super(message); + this.name = 'ForbiddenError'; + this.status = 403; + } +} \ No newline at end of file diff --git a/app/(api)/_utils/response/HttpError.ts b/app/(api)/_utils/response/HttpError.ts new file mode 100644 index 0000000..a4648d5 --- /dev/null +++ b/app/(api)/_utils/response/HttpError.ts @@ -0,0 +1,8 @@ +export default class HttpError extends Error { + public status: number; + + constructor(message: string = 'Internal Server Error') { + super(message); + this.status = 400; + } +} \ No newline at end of file diff --git a/app/(api)/_utils/response/NoContentError.ts b/app/(api)/_utils/response/NoContentError.ts new file mode 100644 index 0000000..2ec637c --- /dev/null +++ b/app/(api)/_utils/response/NoContentError.ts @@ -0,0 +1,9 @@ +import HttpError from './HttpError'; + +export default class NoContentError extends HttpError { + constructor() { + super('No Content Provided'); + this.name = 'NoContentError'; + this.status = 400; // setting to 204 doesn't work w/ Nextjs + } +} \ No newline at end of file diff --git a/app/(api)/_utils/response/NotAuthenticatedError.ts b/app/(api)/_utils/response/NotAuthenticatedError.ts new file mode 100644 index 0000000..2ec637c --- /dev/null +++ b/app/(api)/_utils/response/NotAuthenticatedError.ts @@ -0,0 +1,9 @@ +import HttpError from './HttpError'; + +export default class NoContentError extends HttpError { + constructor() { + super('No Content Provided'); + this.name = 'NoContentError'; + this.status = 400; // setting to 204 doesn't work w/ Nextjs + } +} \ No newline at end of file diff --git a/app/(api)/_utils/response/NotFoundError.ts b/app/(api)/_utils/response/NotFoundError.ts new file mode 100644 index 0000000..437c1b5 --- /dev/null +++ b/app/(api)/_utils/response/NotFoundError.ts @@ -0,0 +1,9 @@ +import HttpError from './HttpError'; + +export default class NotFoundError extends HttpError { + constructor(message: string) { + super(message); + this.name = 'NotFoundError'; + this.status = 404; + } +} \ No newline at end of file diff --git a/app/(pages)/_utils/converFileToMediaItem.ts b/app/(pages)/_utils/convertFileToMediaItem.ts similarity index 100% rename from app/(pages)/_utils/converFileToMediaItem.ts rename to app/(pages)/_utils/convertFileToMediaItem.ts diff --git a/app/(pages)/_utils/uploadMediaItem.ts b/app/(pages)/_utils/uploadMediaItem.ts index a452e6e..08a3899 100644 --- a/app/(pages)/_utils/uploadMediaItem.ts +++ b/app/(pages)/_utils/uploadMediaItem.ts @@ -54,9 +54,8 @@ export default async function uploadMediaItem(mediaItem: MediaItem) { updatedMediaItem.cloudinary_id, getCloudinaryType(updatedMediaItem.type) ); - const errorMsg = `${creationRes.error}${ - deleteStatus.ok ? '' : `\n${deleteStatus.error}` - }`; + const errorMsg = `${creationRes.error}${deleteStatus.ok ? '' : `\n${deleteStatus.error}` + }`; throw new Error(errorMsg); } diff --git a/app/_types/auth/AuthToken.ts b/app/_types/auth/AuthToken.ts new file mode 100644 index 0000000..8f32d18 --- /dev/null +++ b/app/_types/auth/AuthToken.ts @@ -0,0 +1,6 @@ +import User from './User'; + +export interface AuthToken extends User { + iat: number; + exp: number; +} diff --git a/app/_types/auth/User.ts b/app/_types/auth/User.ts new file mode 100644 index 0000000..f71a812 --- /dev/null +++ b/app/_types/auth/User.ts @@ -0,0 +1,5 @@ +export default interface User { + _id: string; + email: string; + password: string; +} diff --git a/app/_types/auth/UserCredentials.ts b/app/_types/auth/UserCredentials.ts new file mode 100644 index 0000000..87987e0 --- /dev/null +++ b/app/_types/auth/UserCredentials.ts @@ -0,0 +1,4 @@ +export default interface UserCredentials { + email: string; + password: string; +} diff --git a/app/_types/content/BaseContentItem.ts b/app/_types/content/BaseContentItem.ts new file mode 100644 index 0000000..efa577e --- /dev/null +++ b/app/_types/content/BaseContentItem.ts @@ -0,0 +1,9 @@ +export default interface BaseContentItem { + _id: string; + _name: string; + _description: string | null; + _published: boolean; + _created_at: string; + _last_modified: string; + [key: string]: any; +} diff --git a/app/_types/media/MediaItem.ts b/app/_types/media/MediaItem.ts new file mode 100644 index 0000000..d43e6dc --- /dev/null +++ b/app/_types/media/MediaItem.ts @@ -0,0 +1,16 @@ +interface MediaItem { + _id: string | null; + cloudinary_id: string | null; + name: string; + type: string; + format: string; + src: string; + alt?: string; + size: number; + width: number | null; + height: number | null; + _created_at?: string | null; + _last_modified?: string | null; +} + +export default MediaItem; diff --git a/app/_types/response/response.ts b/app/_types/response/response.ts new file mode 100644 index 0000000..9ba80ac --- /dev/null +++ b/app/_types/response/response.ts @@ -0,0 +1,5 @@ +export default interface CMSResponse { + ok: boolean; + body: any; + error: string | null; +} diff --git a/app/_utils/schema.ts b/app/_utils/schema.ts new file mode 100644 index 0000000..e220786 --- /dev/null +++ b/app/_utils/schema.ts @@ -0,0 +1,12 @@ +import { ContentSchema } from '@dist/index'; +let schemaJSON; +try { + schemaJSON = require('../../build-assets/schema.json'); +} catch (e) { + schemaJSON = { + schema: {}, + }; +} + +const schema = ContentSchema.fromJSON(schemaJSON); +export default schema; diff --git a/app/_utils/settings.ts b/app/_utils/settings.ts new file mode 100644 index 0000000..f470e0c --- /dev/null +++ b/app/_utils/settings.ts @@ -0,0 +1,11 @@ +let settings; +try { + settings = require('../../build-assets/settings.json'); +} catch (e) { + settings = { + authExpHours: 24, + }; +} +export default settings as { + authExpHours: number; +}; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..4a27102 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,6 @@ +// This is the declaration file that provides type information +import { ContentSchema } from './schema/ContentSchema'; +import { ContentType, FieldType, Field } from './schema/ContentType'; + +// Declare what you export from this module for TS consumers +export { ContentSchema, ContentType, FieldType, Field }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..2e8a15a --- /dev/null +++ b/dist/index.js @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { ContentSchema } = require('./schema/ContentSchema'); +const { ContentType, FieldType } = require('./schema/ContentType'); + +module.exports = { + ContentSchema, + ContentType, + FieldType, +}; diff --git a/dist/schema/ContentSchema.d.ts b/dist/schema/ContentSchema.d.ts new file mode 100644 index 0000000..4ab015a --- /dev/null +++ b/dist/schema/ContentSchema.d.ts @@ -0,0 +1,23 @@ +import { ContentType } from './ContentType'; + +export class ContentSchema { + private schema = {}; + + constructor(schema = {}); + + // Getter to retrieve a ContentType by its key + public get(key: string): ContentType | undefined; + + // Setter to add or update a ContentType + public set(key: string, contentType: ContentType): void; + + // Method to get all content types + public getAll(): { [key: string]: ContentType }; + public getNames(): string[] { + return Object.keys(this.schema); + } + + public toJSON(); + + public static fromJSON(json: object): ContentSchema; +} diff --git a/dist/schema/ContentSchema.js b/dist/schema/ContentSchema.js new file mode 100644 index 0000000..7012fe5 --- /dev/null +++ b/dist/schema/ContentSchema.js @@ -0,0 +1,47 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { ContentType } = require('./ContentType'); + +class ContentSchema { + constructor(schema = {}) { + this.schema = schema; + } + + // Getter to retrieve a ContentType by its key + get(key) { + return this.schema[key]; + } + + // Setter to add or update a ContentType + set(key, contentType) { + this.schema[key] = contentType; + } + + // Method to get all content types + getAll() { + return this.schema; + } + + getNames() { + return Object.keys(this.schema); + } + + toJSON() { + return { + schema: Object.fromEntries( + Object.entries(this.schema).map(([key, value]) => [key, value.toJSON()]) + ), + }; + } + + static fromJSON(json) { + const schema = new ContentSchema(); + Object.entries(json.schema).forEach(([key, value]) => { + const contentType = ContentType.fromJSON(value); + schema.set(key, contentType); + }); + + return schema; + } +} + +module.exports = { ContentSchema }; diff --git a/dist/schema/ContentType.d.ts b/dist/schema/ContentType.d.ts new file mode 100644 index 0000000..8f720d1 --- /dev/null +++ b/dist/schema/ContentType.d.ts @@ -0,0 +1,54 @@ +export interface Field { + name: string; + displayName?: string; + type: FieldType; + required?: boolean; + visible: boolean; + defaultValue?: any; + isPopulated?: (value: any) => boolean; +} + +export const FieldType = { + SHORT_TEXT: 'shortText', + LONG_TEXT: 'longText', + DATE: 'date', + BOOLEAN: 'boolean', + MEDIA_LIST: 'mediaList', +}; + +export class ContentType { + private name: string; + private singularDisplayName: string; + private pluralDisplayName: string; + private fields: { [key: string]: Field } = {}; + + constructor({ + name, + singularDisplayName = name, + pluralDisplayName = singularDisplayName, + }: { + name: string; + singularDisplayName?: string; + pluralDisplayName?: string; + }); + + public createField({ + name, + displayName, + type, + required = false, + visible = true, + defaultValue, + isPopulated, + }: Field); + + public getName(); + public getSingularDisplayName(); + public getPluralDisplayName(); + public getField(fieldName: string); + public getFieldNames(); + public getFieldArray(); + public setFields(fields: { [key: string]: Field }); + public toJSON(); + public static fromJSON(json: object): ContentType; +} diff --git a/dist/schema/ContentType.js b/dist/schema/ContentType.js new file mode 100644 index 0000000..f86e1d9 --- /dev/null +++ b/dist/schema/ContentType.js @@ -0,0 +1,143 @@ +const FieldType = { + SHORT_TEXT: 'shortText', + LONG_TEXT: 'longText', + DATE: 'date', + BOOLEAN: 'boolean', + MEDIA_LIST: 'mediaList', +}; + +const FieldTypeAttributes = { + [FieldType.SHORT_TEXT]: { + defaultValue: '', + isPopulated: Boolean, + }, + [FieldType.LONG_TEXT]: { + defaultValue: '', + isPopulated: Boolean, + }, + [FieldType.DATE]: { + defaultValue: '', + isPopulated: Boolean, + }, + [FieldType.MEDIA_LIST]: { + defaultValue: [], + isPopulated: (value) => value.length !== 0, + }, + [FieldType.BOOLEAN]: { + defaultValue: null, + isPopulated: (value) => value === null, + }, +}; + +const baseFields = { + _name: { + name: '_name', + type: FieldType.SHORT_TEXT, + displayName: 'Internal name', + required: true, + visible: true, + }, + _published: { + name: '_published', + type: FieldType.BOOLEAN, + defaultValue: false, + visible: false, + }, + _created_at: { + name: '_created_at', + type: FieldType.DATE, + visible: false, + }, + _last_modified: { + name: '_last_modified', + type: FieldType.DATE, + visible: false, + }, +}; + +class ContentType { + constructor({ + name, + singularDisplayName = name, + pluralDisplayName = singularDisplayName, + }) { + this.name = name; + this.singularDisplayName = singularDisplayName; + this.pluralDisplayName = pluralDisplayName; + this.fields = JSON.parse(JSON.stringify(baseFields)); + } + + createField({ + name, + displayName, + type, + required = false, + visible = true, + defaultValue, + isPopulated, + }) { + this.fields[name || ''] = { + name: name || '', + displayName: displayName ?? name, + type: type, + required: Boolean(required), + visible: Boolean(visible), + defaultValue: defaultValue ?? FieldTypeAttributes[type].defaultValue, + isPopulated: isPopulated ?? Boolean, + }; + + return this; + } + + getName() { + return this.name; + } + + getSingularDisplayName() { + return this.singularDisplayName; + } + + getPluralDisplayName() { + return this.pluralDisplayName; + } + + getField(fieldName) { + return this.fields[fieldName]; + } + + getFieldNames() { + return Object.keys(this.fields); + } + + getFieldArray() { + return Object.values(this.fields); + } + + setFields(fields) { + this.fields = fields; + } + + toJSON() { + return { + name: this.name, + singularDisplayName: this.singularDisplayName, + pluralDisplayName: this.pluralDisplayName, + fields: this.fields, + }; + } + + static fromJSON(json) { + const contentType = new ContentType({ + name: json.name, + singularDisplayName: json.singularDisplayName, + pluralDisplayName: json.pluralDisplayName, + }); + contentType.setFields(json.fields); + return contentType; + } +} + +module.exports = { + ContentType, + FieldType, +}; diff --git a/package-lock.json b/package-lock.json index 76f2b3f..378dcdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@apollo/server": "^4.11.2", "@as-integrations/next": "^3.2.0", "@prisma/client": "^6.2.1", + "cloudinary": "^2.5.1", "graphql": "^16.10.0", "graphql-tag": "^2.12.6", "mongodb": "^6.3.0", @@ -2469,6 +2470,18 @@ "node": ">=0.8" } }, + "node_modules/cloudinary": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.5.1.tgz", + "integrity": "sha512-CNg6uU53Hl4FEVynkTGpt5bQEAQWDHi3H+Sm62FzKf5uQHipSN2v7qVqS8GRVqeb0T1WNV+22+75DOJeRXYeSQ==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6577,6 +6590,16 @@ "node": ">=6" } }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/package.json b/package.json index 17d7d1f..b7fe59b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@apollo/server": "^4.11.2", "@as-integrations/next": "^3.2.0", "@prisma/client": "^6.2.1", + "cloudinary": "^2.5.1", "graphql": "^16.10.0", "graphql-tag": "^2.12.6", "mongodb": "^6.3.0", From b57d7bebe0cda34b7f90fa0d0c903e24f956ae5d Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:35:39 -0800 Subject: [PATCH 03/10] * Updated and fixed import errors * Not confident in my typescript types. --- app/(api)/_actions/media/createMediaItem.ts | 3 ++- app/(api)/_actions/media/deleteMediaItem.ts | 3 ++- app/(api)/_actions/media/updateMediaItems.ts | 5 +++-- app/(api)/callback/withCallback.ts | 9 +++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 app/(api)/callback/withCallback.ts diff --git a/app/(api)/_actions/media/createMediaItem.ts b/app/(api)/_actions/media/createMediaItem.ts index b8b1d64..6b01d96 100644 --- a/app/(api)/_actions/media/createMediaItem.ts +++ b/app/(api)/_actions/media/createMediaItem.ts @@ -1,7 +1,8 @@ 'use server'; import { createMediaItem } from '@datalib/media/createMediaItem'; import { revalidatePath } from 'next/cache'; -import WithCallback from '@app/(api)/_utils/callback/withCallback'; +// import WithCallback from '@app/(api)/_utils/callback/withCallback'; +import WithCallback from '@utils/callback/withCallback'; export const CreateMediaItem = WithCallback(async (body: object) => { const createMediaItemRes = await createMediaItem(body); diff --git a/app/(api)/_actions/media/deleteMediaItem.ts b/app/(api)/_actions/media/deleteMediaItem.ts index 55eeccb..69f8051 100644 --- a/app/(api)/_actions/media/deleteMediaItem.ts +++ b/app/(api)/_actions/media/deleteMediaItem.ts @@ -1,7 +1,8 @@ 'use server'; import { deleteMediaItem } from '@datalib/media/deleteMediaItem'; import { revalidatePath } from 'next/cache'; -import WithCallback from '@app/(api)/_utils/callback/withCallback'; +// import WithCallback from '@app/(api)/_utils/callback/withCallback'; +import WithCallback from '@utils/callback/withCallback'; export const DeleteMediaItem = WithCallback(async (id: string) => { const deleteMediaRes = await deleteMediaItem(id); diff --git a/app/(api)/_actions/media/updateMediaItems.ts b/app/(api)/_actions/media/updateMediaItems.ts index 1fb8233..9fe209d 100644 --- a/app/(api)/_actions/media/updateMediaItems.ts +++ b/app/(api)/_actions/media/updateMediaItems.ts @@ -1,10 +1,11 @@ 'use server'; import { createMediaItem } from '@datalib/media/createMediaItem'; import { revalidatePath } from 'next/cache'; -import WithCallback from '@app/(api)/_utils/callback/withCallback'; +// import WithCallback from '@app/(api)/_utils/callback/withCallback'; +import WithCallback from '@utils/callback/withCallback'; // import WithCallback from '@utils/callback/withCallback'; export const CreateMediaItem = WithCallback(async (body: object) => { const createMediaItemRes = await createMediaItem(body); revalidatePath('/uploaded-media'); return createMediaItemRes; -}); \ No newline at end of file +}); diff --git a/app/(api)/callback/withCallback.ts b/app/(api)/callback/withCallback.ts new file mode 100644 index 0000000..f22197a --- /dev/null +++ b/app/(api)/callback/withCallback.ts @@ -0,0 +1,9 @@ +import callbacks from '../../_utils/callbacks'; + +export default function WithCallback(func: (...args: unknown[]) => unknown) { + return async (...args: unknown[]) => { + const res = await func(...args); + callbacks.onUpdate(); + return res; + }; +} From ed6c697322ec139a3e81d11d0ffb9bd5987698e5 Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Sun, 2 Mar 2025 23:37:59 -0800 Subject: [PATCH 04/10] Testing --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 378dcdf..88648ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "react-icons": "^4.11.0", "react-quill": "^2.0.0", "readline": "^1.3.0", - "sass": "^1.69.5" + "sass": "^1.85.1" }, "devDependencies": { "@graphql-tools/merge": "^9.0.14", @@ -7217,9 +7217,10 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", - "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", diff --git a/package.json b/package.json index b7fe59b..8c4d62d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "react-icons": "^4.11.0", "react-quill": "^2.0.0", "readline": "^1.3.0", - "sass": "^1.69.5" + "sass": "^1.85.1" }, "devDependencies": { "@graphql-tools/merge": "^9.0.14", From 1673c24766e86088e9f6c79ad033f3d5764b2dae Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:25:09 -0700 Subject: [PATCH 05/10] adjusted to postgres --- app/(api)/_datalib/media/createMediaItem.ts | 19 +++-- app/(api)/_datalib/media/deleteMediaItem.ts | 73 ++++++++++--------- app/(api)/_datalib/media/findMediaItem.ts | 22 +++--- app/(api)/_datalib/media/updateMediaItems.ts | 34 ++++----- app/(api)/_utils/request/getQueries.ts | 48 +++++++++---- app/(api)/_utils/request/parseAndReplace.js | 76 ++++++-------------- prisma/schema.prisma | 9 +++ 7 files changed, 131 insertions(+), 150 deletions(-) diff --git a/app/(api)/_datalib/media/createMediaItem.ts b/app/(api)/_datalib/media/createMediaItem.ts index b4e0da8..0f9947c 100644 --- a/app/(api)/_datalib/media/createMediaItem.ts +++ b/app/(api)/_datalib/media/createMediaItem.ts @@ -1,4 +1,4 @@ -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import prisma from '@datalib/_prisma/client'; import parseAndReplace from '@utils/request/parseAndReplace'; import { HttpError, NoContentError } from '@utils/response/Errors'; import isBodyEmpty from '@utils/request/isBodyEmpty'; @@ -8,18 +8,17 @@ export async function createMediaItem(body: object) { if (isBodyEmpty(body)) { throw new NoContentError(); } - const parsedBody = await parseAndReplace(body); - const db = await getDatabase(); + const parsedBody = await parseAndReplace(body); const currentDate = new Date().toISOString(); - const creationStatus = await db.collection('media').insertOne({ - ...parsedBody, - _last_modified: currentDate, - _created_at: currentDate, - }); - const createdMedia = await db.collection('media').findOne({ - _id: creationStatus.insertedId, + // Insert new media item using Prisma + const createdMedia = await prisma.media.create({ + data: { + ...parsedBody, + lastModified: currentDate, + createdAt: currentDate, + }, }); if (!createdMedia) { diff --git a/app/(api)/_datalib/media/deleteMediaItem.ts b/app/(api)/_datalib/media/deleteMediaItem.ts index 97588d4..b95a9b9 100644 --- a/app/(api)/_datalib/media/deleteMediaItem.ts +++ b/app/(api)/_datalib/media/deleteMediaItem.ts @@ -1,6 +1,4 @@ -import { ObjectId } from 'mongodb'; - -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import prisma from '@datalib/_prisma/client'; import { HttpError, NotFoundError } from '@utils/response/Errors'; import { v2 as cloudinary } from 'cloudinary'; import schema from '@app/_utils/schema'; @@ -12,66 +10,67 @@ cloudinary.config({ api_secret: process.env.CLOUDINARY_API_SECRET, }); -export async function deleteMediaItem(id: string) { +export async function deleteMediaItem( + id: string +): Promise<{ ok: boolean; body: string | null; error: string | null }> { try { - const db = await getDatabase(); - - const objectId = ObjectId.createFromHexString(id); - const mediaItem = await db.collection('media').findOne({ - _id: objectId, + // Find the media item in the database + const mediaItem = await prisma.media.findUnique({ + where: { id }, }); - await cloudinary.uploader.destroy(mediaItem.cloudinary_id, { + if (!mediaItem) { + throw new NotFoundError(`Media item with id: ${id} not found.`); + } + + // Delete from Cloudinary + await cloudinary.uploader.destroy(mediaItem.cloudinaryId, { resource_type: mediaItem.format === 'pdf' ? 'image' : mediaItem.type, }); - const deleteStatus = await db.collection('media').deleteOne({ - _id: objectId, + // Delete from Prisma database + await prisma.media.delete({ + where: { id }, }); - const content_types = schema.getNames(); + const contentTypes = schema.getNames(); + await Promise.all( - content_types.map((content_type: string) => { - const contentSchema = schema.get(content_type); + contentTypes.map(async (contentType: string) => { + const contentSchema = schema.get(contentType); if (!contentSchema) { throw new NotFoundError( - `Content type: ${content_type} does not exist.` + `Content type: ${contentType} does not exist.` ); } + const mediaFields = contentSchema .getFieldArray() .filter((field: Field) => field.type === FieldType.MEDIA_LIST) .map((field: Field) => field.name); - const updatePullList: { [key: string]: any } = {}; - mediaFields.forEach((field: string) => { - updatePullList[field] = objectId; - }); - - const mediaFieldQueries = mediaFields.map((field: string) => ({ - [field]: objectId, - })); - if (mediaFieldQueries.length === 0) { + if (mediaFields.length === 0) { return null; } - return db.collection(content_type).updateMany( - { - $or: mediaFieldQueries, + + // Update all records to remove references to the deleted media item + await prisma[contentType as keyof typeof prisma].updateMany({ + where: { + OR: mediaFields.map((field: string) => ({ + [field]: { has: id }, // Assuming media fields are stored as arrays + })), }, - { - $pull: updatePullList, - } - ); + data: mediaFields.reduce( + (acc: any, field: any) => ({ ...acc, [field]: { set: [] } }), // Remove media item from lists + {} + ), + }); }) ); - if (deleteStatus.deletedCount === 0) { - throw new NotFoundError(`media item with id: ${id} not found.`); - } - return { ok: true, - body: 'media item deleted.', + body: 'Media item deleted.', error: null, }; } catch (error) { diff --git a/app/(api)/_datalib/media/findMediaItem.ts b/app/(api)/_datalib/media/findMediaItem.ts index ec26430..7c5d9bc 100644 --- a/app/(api)/_datalib/media/findMediaItem.ts +++ b/app/(api)/_datalib/media/findMediaItem.ts @@ -1,14 +1,12 @@ -import { ObjectId } from 'mongodb'; -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import prisma from '@datalib/_prisma/client'; import { HttpError, NotFoundError } from '@utils/response/Errors'; -export async function findMediaItem(id: string) { +export async function findMediaItem( + id: string +): Promise<{ ok: boolean; body: any | null; error: string | null }> { try { - const db = await getDatabase(); - const objectId = ObjectId.createFromHexString(id); - - const mediaItem = await db.collection('media').findOne({ - _id: objectId, + const mediaItem = await prisma.media.findUnique({ + where: { id }, }); if (!mediaItem) { @@ -26,10 +24,12 @@ export async function findMediaItem(id: string) { } } -export async function findMediaItems(query: object = {}) { +export async function findMediaItems(query: Record = {}): Promise<{ ok: boolean; body: any[] | null; error: string | null }> { try { - const db = await getDatabase(); - const mediaItems = await db.collection('media').find(query).toArray(); + const mediaItems = await prisma.media.findMany({ + where: query, + }); + return { ok: true, body: mediaItems, error: null }; } catch (error) { const e = error as HttpError; diff --git a/app/(api)/_datalib/media/updateMediaItems.ts b/app/(api)/_datalib/media/updateMediaItems.ts index 55d4ba0..bc02265 100644 --- a/app/(api)/_datalib/media/updateMediaItems.ts +++ b/app/(api)/_datalib/media/updateMediaItems.ts @@ -1,34 +1,24 @@ -import { ObjectId } from 'mongodb'; -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; +import prisma from '@datalib/_prisma/client'; +// import { parseAndReplace } from '@utils/request/parseAndReplace'; import parseAndReplace from '@utils/request/parseAndReplace'; -import { - HttpError, - NoContentError, - NotFoundError, -} from '@utils/response/Errors'; +import { HttpError, NoContentError } from '@utils/response/Errors'; -export async function updateMediaItem(id: string, body = {}) { +export async function updateMediaItem( + id: string, + body = {} +): Promise<{ ok: boolean; body: any | null; error: string | null }> { try { if (!body || Object.keys(body).length === 0) { throw new NoContentError(); } - const db = await getDatabase(); - - const objectId = ObjectId.createFromHexString(id); const updates = await parseAndReplace(body); - updates.$set._last_modified = new Date().toISOString(); - - const updateStatus = await db.collection('media').updateOne( - { - _id: objectId, - }, - updates - ); + updates._last_modified = new Date().toISOString(); - if (updateStatus === null) { - throw new NotFoundError(`Judge with id: ${id} not found.`); - } + const updateStatus = await prisma.media.update({ + where: { id }, + data: updates, + }); return { ok: true, body: updateStatus, error: null }; } catch (error) { diff --git a/app/(api)/_utils/request/getQueries.ts b/app/(api)/_utils/request/getQueries.ts index 4f8e254..20d42cf 100644 --- a/app/(api)/_utils/request/getQueries.ts +++ b/app/(api)/_utils/request/getQueries.ts @@ -1,18 +1,14 @@ import { NextRequest } from 'next/server'; -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; -import { ObjectId } from 'mongodb'; +import prisma from '@datalib/_prisma/client'; +// import { DMMF } from '@prisma/client/runtime'; function typeCast(value: string, type: string) { switch (type) { - case 'int': + case 'Int': return isNaN(+value) ? value : +value; - case 'objectId': - try { - return ObjectId.createFromHexString(value); - } catch { - return value; - } - case 'bool': + case 'String': + return value; + case 'Boolean': if (value === 'true') { return true; } else if (value === 'false') { @@ -29,14 +25,36 @@ export default async function getQueries( request: NextRequest, content_type: string ) { - const db = await getDatabase(); - const query_entries = request.nextUrl.searchParams.entries(); - const schema = (await db.listCollections({ name: content_type }).toArray())[0] - .options.validator; + // Get the schema for the model in Prisma + interface Field { + name: string; + type: string; + } + + interface Schema { + fields: Field[]; + } + + const schema: Schema | undefined = prisma._getDmmf().datamodel.models.find( + (model: { name: string }) => model.name === content_type + ); + + if (!schema) { + throw new Error(`Model ${content_type} not found in Prisma schema.`); + } + + const query_entries = request.nextUrl.searchParams.entries(); const output: { [key: string]: any } = {}; + for (const [key, val] of query_entries) { - output[key] = typeCast(val, schema.$jsonSchema.properties[key]?.bsonType); + // Fetch the field type from the Prisma schema + const field = schema.fields.find((field) => field.name === key); + if (field) { + output[key] = typeCast(val, field.type); + } else { + output[key] = val; // If field is not found in schema, return as is + } } return output; } diff --git a/app/(api)/_utils/request/parseAndReplace.js b/app/(api)/_utils/request/parseAndReplace.js index 5dc1b66..6a659a2 100644 --- a/app/(api)/_utils/request/parseAndReplace.js +++ b/app/(api)/_utils/request/parseAndReplace.js @@ -1,6 +1,8 @@ -import { ObjectId } from 'mongodb'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + -import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; /** * Takes object resembling example below with an "*expandIds" field: * { @@ -14,13 +16,13 @@ import { getDatabase } from '@utils/mongodb/mongoClient.mjs'; */ async function expandIds(obj) { obj = obj['*expandIds']; - const db = await getDatabase(); - const documents = await db - .collection(obj.from) - .find({ - _id: { $in: obj.ids.map((id) => ObjectId.createFromHexString(id)) }, - }) - .toArray(); + const documents = await prisma[obj.from].findMany({ + where: { + id: { + in: obj.ids.map((id) => parseInt(id, 10)), // Assuming ids are integers in Prisma schema + }, + }, + }); return documents; } @@ -37,11 +39,12 @@ async function expandIds(obj) { */ async function expandId(obj) { obj = obj['*expandId']; - const db = await getDatabase(); - const documents = await db.collection(obj.from).findOne({ - _id: ObjectId.createFromHexString(obj.id), + const document = await prisma[obj.from].findUnique({ + where: { + id: parseInt(obj.id, 10), // Assuming id is an integer in Prisma schema + }, }); - return documents; + return document; } /** @@ -52,11 +55,11 @@ async function expandId(obj) { * } * } * - * Returns the array of ids converted to ObjectIds + * Returns the array of ids converted to integers */ async function convertIds(obj) { obj = obj['*convertIds']; - return obj.ids.map((id) => ObjectId.createFromHexString(id)); + return obj.ids.map((id) => parseInt(id, 10)); // Assuming ids are integers in Prisma schema } /** @@ -67,49 +70,12 @@ async function convertIds(obj) { * } * } * - * Returns the id converted to an ObjectId + * Returns the id converted to an integer */ async function convertId(obj) { obj = obj['*convertId']; - return ObjectId.createFromHexString(obj.id); + return parseInt(obj.id, 10); // Assuming id is an integer in Prisma schema } /** - * Searches through a json object and replaces all objects that have a key of - * "*keyword" with the object processed with replaceFunc() - * - * replacements comes in the form of: - * { - * "*keyword": replaceFunc, - * "*keyword2": replaceFunc2 - * } - */ -async function searchAndReplace(obj, replacements) { - if (obj === null || typeof obj !== 'object') { - return obj; - } - for (const [key, val] of Object.entries(obj)) { - let replaced = false; - for (const [keyword, replaceFunc] of Object.entries(replacements)) { - if (val?.[keyword] !== undefined) { - obj[key] = await replaceFunc(val); - replaced = true; - break; - } - } - if (!replaced) { - obj[key] = await searchAndReplace(val, replacements); - } - } - return obj; -} - -export default async function parseAndReplace(obj) { - const res = await searchAndReplace(obj, { - '*expandId': expandId, - '*expandIds': expandIds, - '*convertId': convertId, - '*convertIds': convertIds, - }); - return res; -} + * Searches through a json object and replaces all diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8899bc3..3cadc9c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,6 +19,15 @@ model Inventory { stock_on_order Int } +model Media { + id String @id @default(uuid()) + title String + description String + lastModified DateTime @default(now()) + createdAt DateTime @default(now()) +} + + model Product { id String @id @default(uuid()) inventory Inventory? From 458fd95b949744f75b65e2e5c1158b2bec1a33bf Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Mar 2025 00:36:39 -0700 Subject: [PATCH 06/10] linting --- app/(api)/_datalib/media/deleteMediaItem.ts | 5 ++++- app/(api)/_datalib/media/findMediaItem.ts | 8 ++++++-- app/(api)/_datalib/media/updateMediaItems.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/(api)/_datalib/media/deleteMediaItem.ts b/app/(api)/_datalib/media/deleteMediaItem.ts index b95a9b9..0067201 100644 --- a/app/(api)/_datalib/media/deleteMediaItem.ts +++ b/app/(api)/_datalib/media/deleteMediaItem.ts @@ -61,7 +61,10 @@ export async function deleteMediaItem( })), }, data: mediaFields.reduce( - (acc: any, field: any) => ({ ...acc, [field]: { set: [] } }), // Remove media item from lists + (acc: Record, field: string) => ({ + ...acc, + [field]: { set: [] }, + }), // Remove media item from lists {} ), }); diff --git a/app/(api)/_datalib/media/findMediaItem.ts b/app/(api)/_datalib/media/findMediaItem.ts index 7c5d9bc..b447fca 100644 --- a/app/(api)/_datalib/media/findMediaItem.ts +++ b/app/(api)/_datalib/media/findMediaItem.ts @@ -1,9 +1,11 @@ import prisma from '@datalib/_prisma/client'; import { HttpError, NotFoundError } from '@utils/response/Errors'; +type Media = Awaited>; + export async function findMediaItem( id: string -): Promise<{ ok: boolean; body: any | null; error: string | null }> { +): Promise<{ ok: boolean; body: Media | null; error: string | null }> { try { const mediaItem = await prisma.media.findUnique({ where: { id }, @@ -24,7 +26,9 @@ export async function findMediaItem( } } -export async function findMediaItems(query: Record = {}): Promise<{ ok: boolean; body: any[] | null; error: string | null }> { +export async function findMediaItems( + query: Record = {} +): Promise<{ ok: boolean; body: Media[] | null; error: string | null }> { try { const mediaItems = await prisma.media.findMany({ where: query, diff --git a/app/(api)/_datalib/media/updateMediaItems.ts b/app/(api)/_datalib/media/updateMediaItems.ts index bc02265..aac0c13 100644 --- a/app/(api)/_datalib/media/updateMediaItems.ts +++ b/app/(api)/_datalib/media/updateMediaItems.ts @@ -6,7 +6,7 @@ import { HttpError, NoContentError } from '@utils/response/Errors'; export async function updateMediaItem( id: string, body = {} -): Promise<{ ok: boolean; body: any | null; error: string | null }> { +): Promise<{ ok: boolean; body: object | null; error: string | null }> { try { if (!body || Object.keys(body).length === 0) { throw new NoContentError(); From 571ef94d383254ad00ef27311346572dabce9b44 Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:47:46 -0700 Subject: [PATCH 07/10] updated schema and fixed crud to fix format --- app/(api)/_datalib/_services/Media.ts | 87 ++++++++++++++++++++ app/(api)/_datalib/media/createMediaItem.ts | 37 --------- app/(api)/_datalib/media/deleteMediaItem.ts | 87 -------------------- app/(api)/_datalib/media/findMediaItem.ts | 46 ----------- app/(api)/_datalib/media/updateMediaItems.ts | 32 ------- app/(api)/_types/Media.ts | 30 +++++++ app/(api)/_types/media/MediaItem.ts | 16 ---- app/(api)/_utils/callback/withCallback.ts | 18 ++-- app/(api)/_utils/request/getQueries.ts | 12 +-- prisma/schema.prisma | 18 ++-- 10 files changed, 146 insertions(+), 237 deletions(-) create mode 100644 app/(api)/_datalib/_services/Media.ts delete mode 100644 app/(api)/_datalib/media/createMediaItem.ts delete mode 100644 app/(api)/_datalib/media/deleteMediaItem.ts delete mode 100644 app/(api)/_datalib/media/findMediaItem.ts delete mode 100644 app/(api)/_datalib/media/updateMediaItems.ts create mode 100644 app/(api)/_types/Media.ts delete mode 100644 app/(api)/_types/media/MediaItem.ts diff --git a/app/(api)/_datalib/_services/Media.ts b/app/(api)/_datalib/_services/Media.ts new file mode 100644 index 0000000..80d44d5 --- /dev/null +++ b/app/(api)/_datalib/_services/Media.ts @@ -0,0 +1,87 @@ +import prisma from '../_prisma/client'; +import { MediaInput } from '@datatypes/Media'; +export default class MediaService { + // CREATE + static async create(input: MediaInput) { + const media = await prisma.media.create({ + data: { + cloudinary_id: input.cloudinary_id, + name: input.name, + type: input.type, + format: input.format, + src: input.src, + alt: input.alt || null, + size: input.size, + width: input.width, + height: input.height, + created_at: input.created_at || new Date().toISOString(), + last_modified: input.last_modified || new Date().toISOString(), + }, + }); += return media; + } + + // READ + static async find(id: string) { + return prisma.media.findUnique({ + where: { + id, + }, + }); + } + + // READ + static async findMany(ids: string[]) { + if (!ids || ids.length === 0) { + return prisma.media.findMany(); + } + + return prisma.media.findMany({ + where: { + id: { + in: ids, + }, + }, + }); + } + + // UPDATE + static async update(id: string, input: Partial) { + try { + const media = await prisma.media.update({ + where: { + id, + }, + data: { + cloudinary_id: input.cloudinary_id || undefined, + name: input.name || undefined, + type: input.type || undefined, + format: input.format || undefined, + src: input.src || undefined, + alt: input.alt || null, // If `alt` is not provided, set it to null + size: input.size || undefined, + width: input.width || undefined, + height: input.height || undefined, + last_modified: new Date().toISOString(), // Automatically set `last_modified` to current time + }, + }); + return media; + } catch (e) { + return null; // If update fails, return null + } + } + + // DELETE + static async delete(id: string) { + try { + await prisma.media.delete({ + where: { + id, + }, + }); + return true; + } catch (e) { + return false; + } + } +} diff --git a/app/(api)/_datalib/media/createMediaItem.ts b/app/(api)/_datalib/media/createMediaItem.ts deleted file mode 100644 index 0f9947c..0000000 --- a/app/(api)/_datalib/media/createMediaItem.ts +++ /dev/null @@ -1,37 +0,0 @@ -import prisma from '@datalib/_prisma/client'; -import parseAndReplace from '@utils/request/parseAndReplace'; -import { HttpError, NoContentError } from '@utils/response/Errors'; -import isBodyEmpty from '@utils/request/isBodyEmpty'; - -export async function createMediaItem(body: object) { - try { - if (isBodyEmpty(body)) { - throw new NoContentError(); - } - - const parsedBody = await parseAndReplace(body); - const currentDate = new Date().toISOString(); - - // Insert new media item using Prisma - const createdMedia = await prisma.media.create({ - data: { - ...parsedBody, - lastModified: currentDate, - createdAt: currentDate, - }, - }); - - if (!createdMedia) { - throw new HttpError('Failed to fetch the created item'); - } - - return { ok: true, body: createdMedia, error: null }; - } catch (e) { - const error = e as HttpError; - return { - ok: false, - body: null, - error: error.message || 'Internal Server Error', - }; - } -} diff --git a/app/(api)/_datalib/media/deleteMediaItem.ts b/app/(api)/_datalib/media/deleteMediaItem.ts deleted file mode 100644 index 0067201..0000000 --- a/app/(api)/_datalib/media/deleteMediaItem.ts +++ /dev/null @@ -1,87 +0,0 @@ -import prisma from '@datalib/_prisma/client'; -import { HttpError, NotFoundError } from '@utils/response/Errors'; -import { v2 as cloudinary } from 'cloudinary'; -import schema from '@app/_utils/schema'; -import { FieldType, Field } from '@dist/index'; - -cloudinary.config({ - cloud_name: process.env.CLOUDINARY_NAME, - api_key: process.env.CLOUDINARY_API_KEY, - api_secret: process.env.CLOUDINARY_API_SECRET, -}); - -export async function deleteMediaItem( - id: string -): Promise<{ ok: boolean; body: string | null; error: string | null }> { - try { - // Find the media item in the database - const mediaItem = await prisma.media.findUnique({ - where: { id }, - }); - - if (!mediaItem) { - throw new NotFoundError(`Media item with id: ${id} not found.`); - } - - // Delete from Cloudinary - await cloudinary.uploader.destroy(mediaItem.cloudinaryId, { - resource_type: mediaItem.format === 'pdf' ? 'image' : mediaItem.type, - }); - - // Delete from Prisma database - await prisma.media.delete({ - where: { id }, - }); - - const contentTypes = schema.getNames(); - - await Promise.all( - contentTypes.map(async (contentType: string) => { - const contentSchema = schema.get(contentType); - if (!contentSchema) { - throw new NotFoundError( - `Content type: ${contentType} does not exist.` - ); - } - - const mediaFields = contentSchema - .getFieldArray() - .filter((field: Field) => field.type === FieldType.MEDIA_LIST) - .map((field: Field) => field.name); - - if (mediaFields.length === 0) { - return null; - } - - // Update all records to remove references to the deleted media item - await prisma[contentType as keyof typeof prisma].updateMany({ - where: { - OR: mediaFields.map((field: string) => ({ - [field]: { has: id }, // Assuming media fields are stored as arrays - })), - }, - data: mediaFields.reduce( - (acc: Record, field: string) => ({ - ...acc, - [field]: { set: [] }, - }), // Remove media item from lists - {} - ), - }); - }) - ); - - return { - ok: true, - body: 'Media item deleted.', - error: null, - }; - } catch (error) { - const e = error as HttpError; - return { - ok: false, - body: null, - error: e.message || 'Internal Server Error', - }; - } -} diff --git a/app/(api)/_datalib/media/findMediaItem.ts b/app/(api)/_datalib/media/findMediaItem.ts deleted file mode 100644 index b447fca..0000000 --- a/app/(api)/_datalib/media/findMediaItem.ts +++ /dev/null @@ -1,46 +0,0 @@ -import prisma from '@datalib/_prisma/client'; -import { HttpError, NotFoundError } from '@utils/response/Errors'; - -type Media = Awaited>; - -export async function findMediaItem( - id: string -): Promise<{ ok: boolean; body: Media | null; error: string | null }> { - try { - const mediaItem = await prisma.media.findUnique({ - where: { id }, - }); - - if (!mediaItem) { - throw new NotFoundError(`Media item with id: ${id} not found.`); - } - - return { ok: true, body: mediaItem, error: null }; - } catch (error) { - const e = error as HttpError; - return { - ok: false, - body: null, - error: e.message || 'Internal Server Error', - }; - } -} - -export async function findMediaItems( - query: Record = {} -): Promise<{ ok: boolean; body: Media[] | null; error: string | null }> { - try { - const mediaItems = await prisma.media.findMany({ - where: query, - }); - - return { ok: true, body: mediaItems, error: null }; - } catch (error) { - const e = error as HttpError; - return { - ok: false, - body: null, - error: e.message || 'Internal Server Error', - }; - } -} diff --git a/app/(api)/_datalib/media/updateMediaItems.ts b/app/(api)/_datalib/media/updateMediaItems.ts deleted file mode 100644 index aac0c13..0000000 --- a/app/(api)/_datalib/media/updateMediaItems.ts +++ /dev/null @@ -1,32 +0,0 @@ -import prisma from '@datalib/_prisma/client'; -// import { parseAndReplace } from '@utils/request/parseAndReplace'; -import parseAndReplace from '@utils/request/parseAndReplace'; -import { HttpError, NoContentError } from '@utils/response/Errors'; - -export async function updateMediaItem( - id: string, - body = {} -): Promise<{ ok: boolean; body: object | null; error: string | null }> { - try { - if (!body || Object.keys(body).length === 0) { - throw new NoContentError(); - } - - const updates = await parseAndReplace(body); - updates._last_modified = new Date().toISOString(); - - const updateStatus = await prisma.media.update({ - where: { id }, - data: updates, - }); - - return { ok: true, body: updateStatus, error: null }; - } catch (error) { - const e = error as HttpError; - return { - ok: false, - body: null, - error: e.message || 'Internal Server Error', - }; - } -} diff --git a/app/(api)/_types/Media.ts b/app/(api)/_types/Media.ts new file mode 100644 index 0000000..0d75fad --- /dev/null +++ b/app/(api)/_types/Media.ts @@ -0,0 +1,30 @@ +interface Media { + _id: string | null; + cloudinary_id: string | null; + name: string; + type: string; + format: string; + src: string; + alt?: string; + size: number; + width: number | null; + height: number | null; + created_at?: string | null; + last_modified?: string | null; +} + +export type MediaInput = { + cloudinary_id: string | null; + name: string; + type: string; + format: string; + src: string; + alt?: string; + size: number; + width: number | null; + height: number | null; + created_at?: string | null; + last_modified?: string | null; +}; + +export default Media; diff --git a/app/(api)/_types/media/MediaItem.ts b/app/(api)/_types/media/MediaItem.ts deleted file mode 100644 index d43e6dc..0000000 --- a/app/(api)/_types/media/MediaItem.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface MediaItem { - _id: string | null; - cloudinary_id: string | null; - name: string; - type: string; - format: string; - src: string; - alt?: string; - size: number; - width: number | null; - height: number | null; - _created_at?: string | null; - _last_modified?: string | null; -} - -export default MediaItem; diff --git a/app/(api)/_utils/callback/withCallback.ts b/app/(api)/_utils/callback/withCallback.ts index 6330532..5a262ae 100644 --- a/app/(api)/_utils/callback/withCallback.ts +++ b/app/(api)/_utils/callback/withCallback.ts @@ -1,10 +1,10 @@ -import callbacks from '@app/_utils/callbacks'; +// import callbacks from '@app/_utils/callbacks'; -type CallbackFunction = (...args: any[]) => any; -export default function WithCallback(func: (..._: any) => any) { - return async (...args: any) => { - const res = await func(...args); - callbacks.onUpdate(); - return res; - }; -} +// type CallbackFunction = (...args: any[]) => any; +// export default function WithCallback(func: (..._: any) => any) { +// return async (...args: any) => { +// const res = await func(...args); +// callbacks.onUpdate(); +// return res; +// }; +// } diff --git a/app/(api)/_utils/request/getQueries.ts b/app/(api)/_utils/request/getQueries.ts index 20d42cf..662af1d 100644 --- a/app/(api)/_utils/request/getQueries.ts +++ b/app/(api)/_utils/request/getQueries.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import prisma from '@datalib/_prisma/client'; -// import { DMMF } from '@prisma/client/runtime'; +import { DMMF } from '@prisma/client/runtime'; function typeCast(value: string, type: string) { switch (type) { @@ -36,16 +36,18 @@ export default async function getQueries( fields: Field[]; } - const schema: Schema | undefined = prisma._getDmmf().datamodel.models.find( - (model: { name: string }) => model.name === content_type - ); + const schema: Schema | undefined = prisma + ._getDmmf() + .datamodel.models.find( + (model: { name: string }) => model.name === content_type + ); if (!schema) { throw new Error(`Model ${content_type} not found in Prisma schema.`); } const query_entries = request.nextUrl.searchParams.entries(); - const output: { [key: string]: any } = {}; + const output: { [key: string]: string | number | boolean } = {}; for (const [key, val] of query_entries) { // Fetch the field type from the Prisma schema diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3cadc9c..27e335c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,14 +20,22 @@ model Inventory { } model Media { - id String @id @default(uuid()) - title String - description String - lastModified DateTime @default(now()) - createdAt DateTime @default(now()) + id String @id @default(uuid()) + cloudinary_id String? + name String + type String + format String + src String + alt String? + size Int + width Int? + height Int? + created_at DateTime? @default(now()) + last_modified DateTime? @default(now()) } + model Product { id String @id @default(uuid()) inventory Inventory? From 79d9a2a4eee319d4d3a8bf171de705d7eee334ea Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:48:22 -0700 Subject: [PATCH 08/10] format --- app/(api)/_datalib/_services/Media.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/(api)/_datalib/_services/Media.ts b/app/(api)/_datalib/_services/Media.ts index 80d44d5..01765d4 100644 --- a/app/(api)/_datalib/_services/Media.ts +++ b/app/(api)/_datalib/_services/Media.ts @@ -18,7 +18,7 @@ export default class MediaService { last_modified: input.last_modified || new Date().toISOString(), }, }); -= return media; + return media; } // READ @@ -39,7 +39,7 @@ export default class MediaService { return prisma.media.findMany({ where: { id: { - in: ids, + in: ids, }, }, }); @@ -79,9 +79,9 @@ export default class MediaService { id, }, }); - return true; + return true; } catch (e) { - return false; + return false; } } } From 9ba1dba61d880e7a86facd15a49c3e737fd86a30 Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:55:34 -0700 Subject: [PATCH 09/10] format --- app/(api)/_datalib/_services/Media.ts | 14 +++++++------- app/(api)/_utils/request/getQueries.ts | 2 +- app/(api)/_utils/request/parseAndReplace.js | 3 --- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/(api)/_datalib/_services/Media.ts b/app/(api)/_datalib/_services/Media.ts index 01765d4..dd3ced4 100644 --- a/app/(api)/_datalib/_services/Media.ts +++ b/app/(api)/_datalib/_services/Media.ts @@ -10,18 +10,18 @@ export default class MediaService { type: input.type, format: input.format, src: input.src, - alt: input.alt || null, + alt: input.alt || null, size: input.size, width: input.width, height: input.height, - created_at: input.created_at || new Date().toISOString(), - last_modified: input.last_modified || new Date().toISOString(), + created_at: input.created_at || new Date().toISOString(), + last_modified: input.last_modified || new Date().toISOString(), }, }); return media; } - // READ + // READ static async find(id: string) { return prisma.media.findUnique({ where: { @@ -30,10 +30,10 @@ export default class MediaService { }); } - // READ + // READ static async findMany(ids: string[]) { if (!ids || ids.length === 0) { - return prisma.media.findMany(); + return prisma.media.findMany(); } return prisma.media.findMany({ @@ -58,7 +58,7 @@ export default class MediaService { type: input.type || undefined, format: input.format || undefined, src: input.src || undefined, - alt: input.alt || null, // If `alt` is not provided, set it to null + alt: input.alt || null, // If `alt` is not provided, set it to null size: input.size || undefined, width: input.width || undefined, height: input.height || undefined, diff --git a/app/(api)/_utils/request/getQueries.ts b/app/(api)/_utils/request/getQueries.ts index 662af1d..d3528f8 100644 --- a/app/(api)/_utils/request/getQueries.ts +++ b/app/(api)/_utils/request/getQueries.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import prisma from '@datalib/_prisma/client'; -import { DMMF } from '@prisma/client/runtime'; +// import { DMMF } from '@prisma/client/runtime'; function typeCast(value: string, type: string) { switch (type) { diff --git a/app/(api)/_utils/request/parseAndReplace.js b/app/(api)/_utils/request/parseAndReplace.js index 6a659a2..222fdab 100644 --- a/app/(api)/_utils/request/parseAndReplace.js +++ b/app/(api)/_utils/request/parseAndReplace.js @@ -76,6 +76,3 @@ async function convertId(obj) { obj = obj['*convertId']; return parseInt(obj.id, 10); // Assuming id is an integer in Prisma schema } - -/** - * Searches through a json object and replaces all From 983ecf95e5dde888f0b7bbc883b56acdf0a80291 Mon Sep 17 00:00:00 2001 From: VinBid <115661286+VinBid@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:07:33 -0700 Subject: [PATCH 10/10] deleted unecessary files --- app/(api)/_utils/mongodb/mongoClient.mjs | 18 ----- app/(api)/_utils/request/parseAndReplace.js | 78 --------------------- 2 files changed, 96 deletions(-) delete mode 100644 app/(api)/_utils/mongodb/mongoClient.mjs delete mode 100644 app/(api)/_utils/request/parseAndReplace.js diff --git a/app/(api)/_utils/mongodb/mongoClient.mjs b/app/(api)/_utils/mongodb/mongoClient.mjs deleted file mode 100644 index aeccbf7..0000000 --- a/app/(api)/_utils/mongodb/mongoClient.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { MongoClient } from 'mongodb'; - -const uri = process.env.MONGO_CONNECTION_STRING; -let cachedClient = null; - -export async function getClient() { - if (cachedClient) { - return cachedClient; - } - const client = new MongoClient(uri); - cachedClient = client; - return cachedClient; -} - -export async function getDatabase() { - const client = await getClient(); - return client.db(); -} diff --git a/app/(api)/_utils/request/parseAndReplace.js b/app/(api)/_utils/request/parseAndReplace.js deleted file mode 100644 index 222fdab..0000000 --- a/app/(api)/_utils/request/parseAndReplace.js +++ /dev/null @@ -1,78 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - - -/** - * Takes object resembling example below with an "*expandIds" field: - * { - * "*expandIds": { - * "ids": ["658f94018dac260ae7b17fce", "658f940e8dac260ae7b17fd0"], - * "from": "pokemon" - * } - * } - * When an object that resembles the above object is encountered, return an array of documents - * from the "from" collection - */ -async function expandIds(obj) { - obj = obj['*expandIds']; - const documents = await prisma[obj.from].findMany({ - where: { - id: { - in: obj.ids.map((id) => parseInt(id, 10)), // Assuming ids are integers in Prisma schema - }, - }, - }); - return documents; -} - -/** - * Takes object resembling example below with an "*expandId" field: - * { - * "*expandId": { - * "id": "658f94018dac260ae7b17fce", - * "from": "pokemon" - * } - * } - * When an object that resembles the above object is encountered, return a document - * from the "from" collection - */ -async function expandId(obj) { - obj = obj['*expandId']; - const document = await prisma[obj.from].findUnique({ - where: { - id: parseInt(obj.id, 10), // Assuming id is an integer in Prisma schema - }, - }); - return document; -} - -/** - * Takes object resembling example below with a "*convertIds" field: - * { - * "*convertIds": { - * "ids": ["658f94018dac260ae7b17fce", "658f940e8dac260ae7b17fd0"], - * } - * } - * - * Returns the array of ids converted to integers - */ -async function convertIds(obj) { - obj = obj['*convertIds']; - return obj.ids.map((id) => parseInt(id, 10)); // Assuming ids are integers in Prisma schema -} - -/** - * Takes object resembling example below with a "*convertId" field: - * { - * "*convertId": { - * "id": "658f94018dac260ae7b17fce", - * } - * } - * - * Returns the id converted to an integer - */ -async function convertId(obj) { - obj = obj['*convertId']; - return parseInt(obj.id, 10); // Assuming id is an integer in Prisma schema -}