diff --git a/.env.example b/.env.example index db7d25b..0a75dfc 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ DATABASE_URL="postgresql://..." MINIO_ACCESS_KEY= MINIO_SECRET_KEY= MINIO_ENDPOINT_URL= +MINIO_PORT= MINIO_BUCKET_NAME= BETTER_AUTH_SECRET= diff --git a/docker-compose.services.yaml b/docker-compose.services.yaml index bcd7c9d..63f3920 100644 --- a/docker-compose.services.yaml +++ b/docker-compose.services.yaml @@ -5,7 +5,16 @@ services: COLLECTOR_ZIPKIN_HOST_PORT: 9411 COLLECTOR_OTLP_ENABLED: true ports: - - "16686:16686" + - "6831:6831/udp" # jaeger-agent UDP accept compact thrift protocol + - "6832:6832/udp" # jaeger-agent UDP accept binary thrift protocol + - "5778:5778" # jaeger-agent HTTP serve configs + - "16686:16686" # jaeger-query HTTP serve frontend/API + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "14250:14250" # jaeger-collector gRPC + - "14268:14268" # jaeger-collector HTTP accept spans + - "14269:14269" # jaeger-collector HTTP admin + - "9411:9411" # zipkin-collector HTTP accept spans db: image: postgres:latest diff --git a/drizzle/0000_soft_wraith.sql b/drizzle/0000_lazy_wolfsbane.sql similarity index 74% rename from drizzle/0000_soft_wraith.sql rename to drizzle/0000_lazy_wolfsbane.sql index aa756eb..041c427 100644 --- a/drizzle/0000_soft_wraith.sql +++ b/drizzle/0000_lazy_wolfsbane.sql @@ -62,19 +62,29 @@ CREATE TABLE "auth"."verification" ( "updated_at" timestamp ); --> statement-breakpoint +CREATE TABLE "note_attachments" ( + "id" text PRIMARY KEY NOT NULL, + "title" text DEFAULT 'Untitled', + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + "file_path" text, + "note_id" text NOT NULL +); +--> statement-breakpoint CREATE TABLE "note" ( "id" text PRIMARY KEY NOT NULL, "title" text, "content" text, - "createdAt" timestamp DEFAULT now() NOT NULL, - "updatedAt" timestamp, - "deletedAt" timestamp, - "ownerId" text NOT NULL + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp, + "deleted_at" timestamp, + "owner_id" text NOT NULL ); --> statement-breakpoint ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "note" ADD CONSTRAINT "note_ownerId_user_id_fk" FOREIGN KEY ("ownerId") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "note_attachments" ADD CONSTRAINT "note_attachments_note_id_note_id_fk" FOREIGN KEY ("note_id") REFERENCES "public"."note"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "note" ADD CONSTRAINT "note_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint CREATE INDEX "idx_auth_account_userid" ON "auth"."account" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_auth_account_refreshtokenexpiresat" ON "auth"."account" USING btree ("refresh_token_expires_at");--> statement-breakpoint CREATE INDEX "idx_auth_account_providerid" ON "auth"."account" USING btree ("provider_id");--> statement-breakpoint @@ -83,6 +93,8 @@ CREATE INDEX "idx_auth_session_ip_address" ON "auth"."session" USING btree ("ip_ CREATE INDEX "idx_auth_session_userid" ON "auth"."session" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_auth_verification_identifier" ON "auth"."verification" USING btree ("identifier");--> statement-breakpoint CREATE INDEX "idx_auth_verification_expires_at" ON "auth"."verification" USING btree ("expires_at");--> statement-breakpoint -CREATE INDEX "idx_note_ownerid" ON "note" USING btree ("ownerId");--> statement-breakpoint -CREATE INDEX "idx_note_createdat" ON "note" USING btree ("createdAt");--> statement-breakpoint -CREATE INDEX "idx_note_deletedat" ON "note" USING btree ("deletedAt"); \ No newline at end of file +CREATE INDEX "idx_attachment_attachmentId" ON "note_attachments" USING btree ("note_id");--> statement-breakpoint +CREATE INDEX "idx_attachment_deletedat" ON "note_attachments" USING btree ("deleted_at");--> statement-breakpoint +CREATE INDEX "idx_note_ownerid" ON "note" USING btree ("owner_id");--> statement-breakpoint +CREATE INDEX "idx_note_createdat" ON "note" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idx_note_deletedat" ON "note" USING btree ("deleted_at"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index dfa6446..5ef8956 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "b58e6198-1838-4903-a137-a91a6605653c", + "id": "ef04442a-1d7f-4077-a86f-93dbe5dd3365", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -140,8 +140,12 @@ "tableFrom": "account", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "no action", "onUpdate": "no action" } @@ -333,8 +337,12 @@ "tableFrom": "session", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "no action", "onUpdate": "no action" } @@ -344,7 +352,9 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": ["token"] + "columns": [ + "token" + ] } }, "policies": {}, @@ -405,7 +415,9 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ] } }, "policies": {}, @@ -492,6 +504,102 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.note_attachments": { + "name": "note_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Untitled'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note_id": { + "name": "note_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_attachment_attachmentId": { + "name": "idx_attachment_attachmentId", + "columns": [ + { + "expression": "note_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_attachment_deletedat": { + "name": "idx_attachment_deletedat", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "note_attachments_note_id_note_id_fk": { + "name": "note_attachments_note_id_note_id_fk", + "tableFrom": "note_attachments", + "tableTo": "note", + "columnsFrom": [ + "note_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.note": { "name": "note", "schema": "", @@ -514,27 +622,27 @@ "primaryKey": false, "notNull": false }, - "createdAt": { - "name": "createdAt", + "created_at": { + "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": true, "default": "now()" }, - "updatedAt": { - "name": "updatedAt", + "updated_at": { + "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": false }, - "deletedAt": { - "name": "deletedAt", + "deleted_at": { + "name": "deleted_at", "type": "timestamp", "primaryKey": false, "notNull": false }, - "ownerId": { - "name": "ownerId", + "owner_id": { + "name": "owner_id", "type": "text", "primaryKey": false, "notNull": true @@ -545,7 +653,7 @@ "name": "idx_note_ownerid", "columns": [ { - "expression": "ownerId", + "expression": "owner_id", "isExpression": false, "asc": true, "nulls": "last" @@ -560,7 +668,7 @@ "name": "idx_note_createdat", "columns": [ { - "expression": "createdAt", + "expression": "created_at", "isExpression": false, "asc": true, "nulls": "last" @@ -575,7 +683,7 @@ "name": "idx_note_deletedat", "columns": [ { - "expression": "deletedAt", + "expression": "deleted_at", "isExpression": false, "asc": true, "nulls": "last" @@ -588,13 +696,17 @@ } }, "foreignKeys": { - "note_ownerId_user_id_fk": { - "name": "note_ownerId_user_id_fk", + "note_owner_id_user_id_fk": { + "name": "note_owner_id_user_id_fk", "tableFrom": "note", "tableTo": "user", "schemaTo": "auth", - "columnsFrom": ["ownerId"], - "columnsTo": ["id"], + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "no action", "onUpdate": "no action" } @@ -619,4 +731,4 @@ "schemas": {}, "tables": {} } -} +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 13ebf20..336cc10 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,9 +5,9 @@ { "idx": 0, "version": "7", - "when": 1737390835611, - "tag": "0000_soft_wraith", + "when": 1737413866885, + "tag": "0000_lazy_wolfsbane", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts index 0893eaa..87380b4 100644 --- a/src/api/routes/index.ts +++ b/src/api/routes/index.ts @@ -1,4 +1,5 @@ import { Elysia, t } from "elysia"; import { noteRouter } from "./note/note.route"; +import { attachmentRouter } from "./note/attachments/attachment.route"; -export const router = new Elysia().use(noteRouter); +export const router = new Elysia().use(noteRouter).use(attachmentRouter); diff --git a/src/api/routes/note/attachments/attachment.controller.ts b/src/api/routes/note/attachments/attachment.controller.ts new file mode 100644 index 0000000..1daddfe --- /dev/null +++ b/src/api/routes/note/attachments/attachment.controller.ts @@ -0,0 +1,169 @@ +import { and, eq, isNull } from "drizzle-orm"; +import { db } from "../../../../db"; +import { attachment, note } from "../../../../db/schema/note"; +import { CreateAttachmentType } from "./attachment.model"; +import { NoteController } from "../note.controller"; +import { createId } from "@paralleldrive/cuid2"; +import { getSignedUrl, uploadFileAndGetUrl } from "../../../../lib/storage/storage"; +import { error } from "elysia"; + +export class AttachmentController { + async createAttachment(new_attachment: CreateAttachmentType, ownerId:string) { + const note = new NoteController(); + + const existingNote = await note.getNoteById(new_attachment.noteId, ownerId); + if (existingNote.data.length===0){ + throw error(403, { + success: false, + data: [], + message: "User do not have access", + error: "FORBIDDEN", + }); + } + const filePath = "attachments/"+existingNote.data[0].id+"/file_"+createId()+"-"+new_attachment.file.name; + const attachmentUrl = await uploadFileAndGetUrl(filePath, new_attachment.file) + const new_attachment_data = { ...new_attachment, noteId: new_attachment.noteId, filePath:filePath }; + const result = await db + .insert(attachment) + .values(new_attachment_data) + .returning({ + id: attachment.id, + title: attachment.title, + noteId: attachment.noteId, + createdAt: attachment.createdAt + }) + .execute(); + + const resultWithAttachment = { + attachmentUrl:attachmentUrl, + ...result[0] + } + return { + success: true, + data: [resultWithAttachment], + message: "Attachment created successfully", + error: null, + }; + } + + async getAttachmentsByNoteId(noteId: string, ownerId: string, limit: number = 10, offset: number = 0) { + const result = await db + .select({ + id: attachment.id, + title: attachment.title, + filePath: attachment.filePath, + noteId: attachment.noteId, + createdAt: attachment.createdAt, + }) + .from(attachment) + .leftJoin(note, eq(note.id, attachment.noteId)) + .where(and(eq(attachment.noteId, noteId), isNull(attachment.deletedAt), eq(note.ownerId, ownerId))) + .limit(limit) + .offset(offset) + .execute(); + + const allAttachments = []; + for (let i = 0; i userMiddleware(request)) + .onError(({ path, error, code }) => { + console.error(error); + return { + message: path+" Error:"+code, + success: false, + data: null, + error: code.toString(), + }; + }) + .get( + "", + async ({ attachment, user, query }) => { + return await attachment.getAttachmentsByNoteId(query.noteId, user.id, query.limit, query.offset); + }, + { + query: t.Object({ + noteId:t.String(), + limit: t.Optional(t.Number()), + offset: t.Optional(t.Number()), + }), + response: getAttachmentResponses, + detail: { + description: "Get all attachments of the user", + summary: "Get all attachments", + }, + }, + ) + .get( + "/:id", + async ({ attachment, user, params }) => { + return await attachment.getAttachmentById(params.id, user.id); + }, + { + params: t.Object({ + id: t.String(), + }), + response: getAttachmentResponses, + detail: { + description: "Get a attachment by Id", + summary: "Get a attachment", + }, + }, + ) + .post( + "", + async ({ body, attachment, user }) => { + return await attachment.createAttachment(body, user.id); + }, + { + body: createAttachmentSchema, + response: getAttachmentResponses, + detail: { + description: "Create a new attachment", + summary: "Create a attachment", + }, + }, + ) + .delete( + "/:id", + async ({ attachment, user, params: { id } }) => { + return await attachment.deleteAttachmentById(id, user.id); + }, + { + params: t.Object({ + id: t.String(), + }), + response: deleteAttachmentResponses, + detail: { + description: "Delete a attachment by Id", + summary: "Delete a attachment", + }, + }, + ) + .delete( + "", + async ({ attachment, user, query }) => { + return await attachment.deleteAllAttachmentsByNoteId(query.noteId, user.id); + }, + { + query: t.Object({ + noteId:t.String(), + }), + response: deleteAttachmentResponses, + detail: { + description: "Delete all attachments of an user", + summary: "Delete all attachments", + }, + }, + ); diff --git a/src/api/routes/note/attachments/attachment.test.ts b/src/api/routes/note/attachments/attachment.test.ts new file mode 100644 index 0000000..b4f7704 --- /dev/null +++ b/src/api/routes/note/attachments/attachment.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it, beforeAll } from "bun:test"; +import { testClientApp } from "../../../../../test/client"; + +let attachmentId: string; +let noteId: string; + +// Helper function to create a test file +const createTestFile = (content: string, filename: string = "test.txt") => { + return new File([content], filename, { type: "text/plain" }); +}; + +describe("Attachment", () => { + // Create a note first since attachments need a noteId + beforeAll(async () => { + const { data } = await testClientApp.api.note.post({ + title: "test note for attachments", + content: "description", + }); + if (!data?.data) { + throw new Error("create note api did not return data"); + } + noteId = data.data[0].id; + }); + + // Create an attachment + it("Create Attachment", async () => { + const testFile = createTestFile("Hello, World!", "test.txt"); + const { data } = await testClientApp.api.attachment.post({ + title: "test attachment", + file: testFile, + noteId: noteId, + }); + + if (!data?.data) { + throw new Error("create attachment api did not return data"); + } + + attachmentId = data.data[0].id; + expect(data.data[0].title).toBe("test attachment"); + expect(data.data[0].noteId).toBe(noteId); + expect(data.data[0].attachmentUrl).toBeDefined(); + }); + + // Get all attachments for a note + it("Get All Attachments for a Note", async () => { + const { data } = await testClientApp.api.attachment.get({ + query: { + noteId: noteId, + limit: 10, + offset: 0 + }, + }); + expect(data?.success).toBe(true); + expect(data?.data).toHaveLength(1); + expect(data?.data[0].id).toBe(attachmentId); + }); + + // Get single attachment + it("Get Created Attachment", async () => { + const { data } = await testClientApp.api.attachment({ id: attachmentId }).get(); + expect(data?.success).toBe(true); + expect(data?.data[0].id).toBe(attachmentId); + expect(data?.data[0].noteId).toBe(noteId); + expect(data?.data[0].attachmentUrl).toBeDefined(); + }); + + // Delete single attachment + it("Delete Single Attachment", async () => { + // First create a new attachment to delete + const testFile = createTestFile("Delete me!", "delete.txt"); + const { data: createData } = await testClientApp.api.attachment.post({ + title: "attachment to delete", + file: testFile, + noteId: noteId, + }); + + if (!createData?.data) { + throw new Error("CreateData should not be null"); + } + + const deleteAttachmentId = createData?.data[0].id; + if (!deleteAttachmentId) { + throw new Error("Failed to receive attachmentId in delete attachment test"); + } + + // Delete the attachment + const { data: deleteData } = await testClientApp.api + .attachment({ id: deleteAttachmentId }) + .delete(); + expect(deleteData?.success).toBe(true); + + // Verify attachment is deleted by trying to fetch it + const { data: verifyData } = await testClientApp.api + .attachment({ id: deleteAttachmentId }) + .get(); + expect(verifyData?.data).toHaveLength(0); + }); + + // Delete all attachments for a note + it("Delete All Attachments for a Note", async () => { + // First create multiple attachments + const file1 = createTestFile("Content 1", "file1.txt"); + const file2 = createTestFile("Content 2", "file2.txt"); + + await testClientApp.api.attachment.post({ + title: "attachment 1", + file: file1, + noteId: noteId, + }); + await testClientApp.api.attachment.post({ + title: "attachment 2", + file: file2, + noteId: noteId, + }); + + // Delete all attachments for the note + const { data: deleteData } = await testClientApp.api.attachment.delete({},{ + query: { noteId: noteId } + }); + expect(deleteData?.success).toBe(true); + + // Verify all attachments are deleted + const { data: verifyData } = await testClientApp.api.attachment.get({ + query: { + noteId: noteId, + limit: 10, + offset: 0 + }, + }); + expect(verifyData?.data).toHaveLength(0); + }); + + + + // Error cases + it("Should handle invalid attachment ID", async () => { + const invalidId = "invalid-id"; + const { data, error } = await testClientApp.api.attachment({ id: invalidId }).get(); + expect(data?.data?.length).toBe(0); + }); + + it("Should handle invalid note ID when creating attachment", async () => { + const invalidNoteId = "invalid-note-id"; + const testFile = createTestFile("Test content", "test.txt"); + const { data, error } = await testClientApp.api.attachment.post({ + title: "test attachment", + file: testFile, + noteId: invalidNoteId, + }); + expect(error?.status).toBe(500); + }); + + // Clean up - delete the test note after all tests + it("Clean up test note", async () => { + const { data } = await testClientApp.api.note({ id: noteId }).delete(); + expect(data?.success).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/api/routes/note/note.model.ts b/src/api/routes/note/note.model.ts index 15f7d88..1aea15b 100644 --- a/src/api/routes/note/note.model.ts +++ b/src/api/routes/note/note.model.ts @@ -13,7 +13,7 @@ export type CreateNoteType = Pick< "title" | "content" >; -export const createNoteSchema = t.Pick(NoteSchema, ["title", "content"]); +export const createNoteSchema =t.Partial(t.Pick(NoteSchema, ["title", "content"])) export const getNoteResponses = { 200: t.Object( diff --git a/src/api/routes/note/note.route.ts b/src/api/routes/note/note.route.ts index a677d42..44221f4 100644 --- a/src/api/routes/note/note.route.ts +++ b/src/api/routes/note/note.route.ts @@ -50,8 +50,8 @@ export const noteRouter = new Elysia({ ) .get( "/:id", - async ({ note, user, params: { id } }) => { - return await note.getNoteById(id, user.id); + async ({ note, user, params }) => { + return await note.getNoteById(params.id, user.id); }, { params: t.Object({ diff --git a/src/db/schema/note.ts b/src/db/schema/note.ts index 3b23f8d..0e1149b 100644 --- a/src/db/schema/note.ts +++ b/src/db/schema/note.ts @@ -10,10 +10,10 @@ export const note = pgTable( .$defaultFn(() => `note_${createId()}`), title: text("title"), content: text("content"), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp(), - deletedAt: timestamp(), - ownerId: text() + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at"), + deletedAt: timestamp("deleted_at"), + ownerId: text("owner_id") .notNull() .references(() => user.id), }, @@ -24,3 +24,24 @@ export const note = pgTable( index("idx_note_deletedat").on(table.deletedAt), ], ); + + +export const attachment = pgTable( + "note_attachments", + { + id: text("id") + .primaryKey() + .$defaultFn(() => `attachment_${createId()}`), + title: text("title").default("Untitled"), + createdAt: timestamp("created_at").notNull().defaultNow(), + deletedAt: timestamp("deleted_at"), + filePath: text("file_path"), + noteId: text("note_id") + .notNull() + .references(() => note.id), + }, + (table) => [ + index("idx_attachment_attachmentId").on(table.noteId), + index("idx_attachment_deletedat").on(table.deletedAt), + ], +); diff --git a/src/lib/storage/storage.ts b/src/lib/storage/storage.ts index 2cdfd75..ed973aa 100644 --- a/src/lib/storage/storage.ts +++ b/src/lib/storage/storage.ts @@ -7,6 +7,7 @@ import { getMinioConfig } from "../utils/env"; const minioConfig = getMinioConfig(); const minioClient = new Client({ endPoint: minioConfig.MINIO_ENDPOINT_URL, + port: +minioConfig.MINIO_PORT, useSSL: false, accessKey: minioConfig.MINIO_ACCESS_KEY, secretKey: minioConfig.MINIO_SECRET_KEY, diff --git a/src/lib/utils/env.ts b/src/lib/utils/env.ts index 43f792c..83fefa8 100644 --- a/src/lib/utils/env.ts +++ b/src/lib/utils/env.ts @@ -12,7 +12,8 @@ const envSchema = z.object({ // MinIO MINIO_ACCESS_KEY: z.string(), MINIO_SECRET_KEY: z.string().min(8), - MINIO_ENDPOINT_URL: z.string().url(), + MINIO_ENDPOINT_URL: z.string(), + MINIO_PORT: z.string().max(5), MINIO_BUCKET_NAME: z.string(), // Auth @@ -98,6 +99,7 @@ export const getMinioConfig = (): Pick< | "MINIO_ACCESS_KEY" | "MINIO_SECRET_KEY" | "MINIO_ENDPOINT_URL" + | "MINIO_PORT" | "MINIO_BUCKET_NAME" > => { const config = getConfig(); @@ -105,6 +107,7 @@ export const getMinioConfig = (): Pick< MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: config.MINIO_SECRET_KEY, MINIO_ENDPOINT_URL: config.MINIO_ENDPOINT_URL, + MINIO_PORT: config.MINIO_PORT, MINIO_BUCKET_NAME: config.MINIO_BUCKET_NAME, }; };