say-under-me 1.4.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -37,6 +37,8 @@ type CrudConfig<T = any> = {
37
37
  ownerField?: string;
38
38
  adminRole?: string;
39
39
  hooks?: LifecycleHooks<T>;
40
+ imageFields?: string[];
41
+ publicDir?: string;
40
42
  };
41
43
  type PaginatedResponse<T> = {
42
44
  data: T[];
@@ -47,6 +49,17 @@ type PaginatedResponse<T> = {
47
49
  totalPages: number;
48
50
  };
49
51
  };
52
+ type UploadConfig = {
53
+ uploadDir?: string;
54
+ maxSize?: number;
55
+ allowedTypes?: string[];
56
+ };
57
+ type UploadResult = {
58
+ url: string;
59
+ name: string;
60
+ size: number;
61
+ type: string;
62
+ };
50
63
  declare class ForbiddenError extends Error {
51
64
  constructor(message?: string);
52
65
  }
@@ -108,6 +121,10 @@ declare class PrismaCrud<T> {
108
121
  * Deletes a record by ID with optional lifecycle hooks.
109
122
  */
110
123
  delete(id: string | number, context?: UserContext): Promise<any>;
124
+ /**
125
+ * Deletes a file from the filesystem if it belongs to the uploads directory.
126
+ */
127
+ private deleteLocalFile;
111
128
  /**
112
129
  * Logic to detect if an ID is a number (Int) or string (UUID/CUID).
113
130
  * This allows the API to handle both `id=1` and `id=uuid-string` automatically.
@@ -133,4 +150,23 @@ declare function createApiHandler<T>(crud: PrismaCrud<T>, options?: {
133
150
  DELETE: (req: NextRequest, context: any) => Promise<NextResponse<unknown>>;
134
151
  };
135
152
 
136
- export { type CrudConfig, ForbiddenError, type LifecycleHooks, type PaginatedResponse, type ParseOptions, type PermissionConfig, PrismaCrud, type PrismaQuery, type UserContext, createApiHandler, parsePrismaQuery, sayHello, type sayHelloProps };
153
+ /**
154
+ * Creates a Next.js App Router handler for file uploads and management.
155
+ * POST: Upload new files
156
+ * GET: List existing files
157
+ */
158
+ declare function createUploadHandler(config?: UploadConfig): {
159
+ GET: () => Promise<NextResponse<any[]> | NextResponse<{
160
+ error: string;
161
+ }>>;
162
+ POST: (req: NextRequest) => Promise<NextResponse<{
163
+ error: string;
164
+ }> | NextResponse<UploadResult | UploadResult[]>>;
165
+ DELETE: (req: NextRequest) => Promise<NextResponse<{
166
+ error: string;
167
+ }> | NextResponse<{
168
+ success: boolean;
169
+ }>>;
170
+ };
171
+
172
+ export { type CrudConfig, ForbiddenError, type LifecycleHooks, type PaginatedResponse, type ParseOptions, type PermissionConfig, PrismaCrud, type PrismaQuery, type UploadConfig, type UploadResult, type UserContext, createApiHandler, createUploadHandler, parsePrismaQuery, sayHello, type sayHelloProps };
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ type CrudConfig<T = any> = {
37
37
  ownerField?: string;
38
38
  adminRole?: string;
39
39
  hooks?: LifecycleHooks<T>;
40
+ imageFields?: string[];
41
+ publicDir?: string;
40
42
  };
41
43
  type PaginatedResponse<T> = {
42
44
  data: T[];
@@ -47,6 +49,17 @@ type PaginatedResponse<T> = {
47
49
  totalPages: number;
48
50
  };
49
51
  };
52
+ type UploadConfig = {
53
+ uploadDir?: string;
54
+ maxSize?: number;
55
+ allowedTypes?: string[];
56
+ };
57
+ type UploadResult = {
58
+ url: string;
59
+ name: string;
60
+ size: number;
61
+ type: string;
62
+ };
50
63
  declare class ForbiddenError extends Error {
51
64
  constructor(message?: string);
52
65
  }
@@ -108,6 +121,10 @@ declare class PrismaCrud<T> {
108
121
  * Deletes a record by ID with optional lifecycle hooks.
109
122
  */
110
123
  delete(id: string | number, context?: UserContext): Promise<any>;
124
+ /**
125
+ * Deletes a file from the filesystem if it belongs to the uploads directory.
126
+ */
127
+ private deleteLocalFile;
111
128
  /**
112
129
  * Logic to detect if an ID is a number (Int) or string (UUID/CUID).
113
130
  * This allows the API to handle both `id=1` and `id=uuid-string` automatically.
@@ -133,4 +150,23 @@ declare function createApiHandler<T>(crud: PrismaCrud<T>, options?: {
133
150
  DELETE: (req: NextRequest, context: any) => Promise<NextResponse<unknown>>;
134
151
  };
135
152
 
136
- export { type CrudConfig, ForbiddenError, type LifecycleHooks, type PaginatedResponse, type ParseOptions, type PermissionConfig, PrismaCrud, type PrismaQuery, type UserContext, createApiHandler, parsePrismaQuery, sayHello, type sayHelloProps };
153
+ /**
154
+ * Creates a Next.js App Router handler for file uploads and management.
155
+ * POST: Upload new files
156
+ * GET: List existing files
157
+ */
158
+ declare function createUploadHandler(config?: UploadConfig): {
159
+ GET: () => Promise<NextResponse<any[]> | NextResponse<{
160
+ error: string;
161
+ }>>;
162
+ POST: (req: NextRequest) => Promise<NextResponse<{
163
+ error: string;
164
+ }> | NextResponse<UploadResult | UploadResult[]>>;
165
+ DELETE: (req: NextRequest) => Promise<NextResponse<{
166
+ error: string;
167
+ }> | NextResponse<{
168
+ success: boolean;
169
+ }>>;
170
+ };
171
+
172
+ export { type CrudConfig, ForbiddenError, type LifecycleHooks, type PaginatedResponse, type ParseOptions, type PermissionConfig, PrismaCrud, type PrismaQuery, type UploadConfig, type UploadResult, type UserContext, createApiHandler, createUploadHandler, parsePrismaQuery, sayHello, type sayHelloProps };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,6 +33,7 @@ __export(index_exports, {
23
33
  ForbiddenError: () => ForbiddenError,
24
34
  PrismaCrud: () => PrismaCrud,
25
35
  createApiHandler: () => createApiHandler,
36
+ createUploadHandler: () => createUploadHandler,
26
37
  parsePrismaQuery: () => parsePrismaQuery,
27
38
  sayHello: () => sayHello
28
39
  });
@@ -118,7 +129,15 @@ function parsePrismaQuery(queryString, options = {}) {
118
129
  const rawVal = value.substring(dotIndex + 1);
119
130
  const prismaOp = OPERATOR_MAP[op];
120
131
  if (prismaOp) {
121
- whereTarget[targetKey] = { [prismaOp]: parseValue(rawVal, op) };
132
+ const parsedVal = parseValue(rawVal, op);
133
+ if (["contains", "startsWith", "endsWith"].includes(prismaOp) && typeof parsedVal === "string") {
134
+ whereTarget[targetKey] = {
135
+ [prismaOp]: parsedVal,
136
+ mode: "insensitive"
137
+ };
138
+ } else {
139
+ whereTarget[targetKey] = { [prismaOp]: parsedVal };
140
+ }
122
141
  return;
123
142
  }
124
143
  }
@@ -184,6 +203,8 @@ function parseValue(val, op) {
184
203
  }
185
204
 
186
205
  // src/core/prisma-crud.ts
206
+ var import_promises = require("fs/promises");
207
+ var import_path = require("path");
187
208
  var PrismaCrud = class {
188
209
  /**
189
210
  * @param model -The Prisma model delegate (e.g., prisma.user)
@@ -286,7 +307,29 @@ var PrismaCrud = class {
286
307
  if (!this.checkPermission("update", context)) throw new ForbiddenError();
287
308
  try {
288
309
  if (schema) data = schema.parse(data);
289
- const where = this.applySecurity({ id: this.parseId(id) }, context);
310
+ const parsedId = this.parseId(id);
311
+ const where = this.applySecurity({ id: parsedId }, context);
312
+ if (this.config.imageFields && this.config.imageFields.length > 0) {
313
+ const oldRecord = await this.model.findUnique({ where });
314
+ if (oldRecord) {
315
+ for (const field of this.config.imageFields) {
316
+ const newValue = data[field];
317
+ const oldValue = oldRecord[field];
318
+ if (newValue === void 0) continue;
319
+ if (typeof oldValue === "string" && typeof newValue === "string") {
320
+ if (oldValue !== newValue) {
321
+ await this.deleteLocalFile(oldValue);
322
+ }
323
+ } else if (Array.isArray(oldValue)) {
324
+ const newArray = Array.isArray(newValue) ? newValue : [];
325
+ const removedImages = oldValue.filter((img) => !newArray.includes(img));
326
+ for (const img of removedImages) {
327
+ await this.deleteLocalFile(img);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
290
333
  if (this.config.hooks?.beforeUpdate) {
291
334
  data = await this.config.hooks.beforeUpdate(data);
292
335
  }
@@ -309,8 +352,24 @@ var PrismaCrud = class {
309
352
  async delete(id, context) {
310
353
  if (!this.checkPermission("delete", context)) throw new ForbiddenError();
311
354
  try {
312
- const where = this.applySecurity({ id: this.parseId(id) }, context);
313
355
  const parsedId = this.parseId(id);
356
+ const where = this.applySecurity({ id: parsedId }, context);
357
+ if (this.config.imageFields && this.config.imageFields.length > 0) {
358
+ const record = await this.model.findUnique({ where });
359
+ if (record) {
360
+ for (const field of this.config.imageFields) {
361
+ const value = record[field];
362
+ if (!value) continue;
363
+ if (Array.isArray(value)) {
364
+ for (const img of value) {
365
+ await this.deleteLocalFile(img);
366
+ }
367
+ } else if (typeof value === "string") {
368
+ await this.deleteLocalFile(value);
369
+ }
370
+ }
371
+ }
372
+ }
314
373
  if (this.config.hooks?.beforeDelete) {
315
374
  await this.config.hooks.beforeDelete(parsedId);
316
375
  }
@@ -324,6 +383,20 @@ var PrismaCrud = class {
324
383
  throw error;
325
384
  }
326
385
  }
386
+ /**
387
+ * Deletes a file from the filesystem if it belongs to the uploads directory.
388
+ */
389
+ async deleteLocalFile(relativeUrl) {
390
+ try {
391
+ if (!relativeUrl || relativeUrl.startsWith("http")) return;
392
+ const publicDir = this.config.publicDir || "public";
393
+ const absolutePath = (0, import_path.join)(process.cwd(), publicDir, relativeUrl);
394
+ await (0, import_promises.unlink)(absolutePath);
395
+ console.log(`[underme] Deleted old file: ${absolutePath}`);
396
+ } catch (error) {
397
+ console.warn(`[underme] Failed to delete file at ${relativeUrl}:`, error);
398
+ }
399
+ }
327
400
  /**
328
401
  * Logic to detect if an ID is a number (Int) or string (UUID/CUID).
329
402
  * This allows the API to handle both `id=1` and `id=uuid-string` automatically.
@@ -417,11 +490,103 @@ function createApiHandler(crud, options = {}) {
417
490
  };
418
491
  return { GET, POST, PATCH, DELETE };
419
492
  }
493
+
494
+ // src/adapters/upload-handler.ts
495
+ var import_server2 = require("next/server");
496
+ var import_promises2 = require("fs/promises");
497
+ var import_path2 = __toESM(require("path"));
498
+ var import_crypto = __toESM(require("crypto"));
499
+ function createUploadHandler(config = {}) {
500
+ const uploadDir = config.uploadDir || "public/uploads";
501
+ const maxSize = config.maxSize || 5 * 1024 * 1024;
502
+ const allowedTypes = config.allowedTypes || ["image/jpeg", "image/png", "image/webp", "image/gif"];
503
+ const GET = async () => {
504
+ try {
505
+ const absoluteUploadDir = import_path2.default.join(process.cwd(), uploadDir);
506
+ await (0, import_promises2.mkdir)(absoluteUploadDir, { recursive: true });
507
+ const files = await (0, import_promises2.readdir)(absoluteUploadDir);
508
+ const results = await Promise.all(
509
+ files.map(async (filename) => {
510
+ const filePath = import_path2.default.join(absoluteUploadDir, filename);
511
+ const fileStat = await (0, import_promises2.stat)(filePath);
512
+ if (fileStat.isDirectory()) return null;
513
+ return {
514
+ url: `/${uploadDir.replace("public/", "")}/${filename}`,
515
+ name: filename,
516
+ size: fileStat.size,
517
+ atime: fileStat.atime,
518
+ mtime: fileStat.mtime
519
+ };
520
+ })
521
+ );
522
+ return import_server2.NextResponse.json(results.filter((f) => f !== null).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()));
523
+ } catch (error) {
524
+ return import_server2.NextResponse.json({ error: "Failed to list uploads: " + error.message }, { status: 500 });
525
+ }
526
+ };
527
+ const POST = async (req) => {
528
+ try {
529
+ const formData = await req.formData();
530
+ const files = formData.getAll("file");
531
+ if (!files || files.length === 0) {
532
+ return import_server2.NextResponse.json({ error: "No files uploaded" }, { status: 400 });
533
+ }
534
+ const results = [];
535
+ const absoluteUploadDir = import_path2.default.join(process.cwd(), uploadDir);
536
+ await (0, import_promises2.mkdir)(absoluteUploadDir, { recursive: true });
537
+ for (const file of files) {
538
+ if (file.size > maxSize) {
539
+ return import_server2.NextResponse.json({
540
+ error: `File ${file.name} is too large. Max size is ${maxSize / (1024 * 1024)}MB`
541
+ }, { status: 400 });
542
+ }
543
+ if (!allowedTypes.includes(file.type)) {
544
+ return import_server2.NextResponse.json({
545
+ error: `File type ${file.type} is not allowed.`
546
+ }, { status: 400 });
547
+ }
548
+ const buffer = Buffer.from(await file.arrayBuffer());
549
+ const extension = import_path2.default.extname(file.name);
550
+ const filename = `${import_crypto.default.randomUUID()}${extension}`;
551
+ const filePath = import_path2.default.join(absoluteUploadDir, filename);
552
+ await (0, import_promises2.writeFile)(filePath, buffer);
553
+ results.push({
554
+ url: `/${uploadDir.replace("public/", "")}/${filename}`,
555
+ name: filename,
556
+ // Original filename is replaced with UUID for safety
557
+ size: file.size,
558
+ type: file.type
559
+ });
560
+ }
561
+ return import_server2.NextResponse.json(files.length === 1 ? results[0] : results);
562
+ } catch (error) {
563
+ console.error("Upload Handler Error:", error);
564
+ return import_server2.NextResponse.json({ error: "Upload failed: " + error.message }, { status: 500 });
565
+ }
566
+ };
567
+ const DELETE = async (req) => {
568
+ try {
569
+ const { searchParams } = new URL(req.url);
570
+ const filename = searchParams.get("file");
571
+ if (!filename) {
572
+ return import_server2.NextResponse.json({ error: "Filename is required" }, { status: 400 });
573
+ }
574
+ const sanitizedFilename = import_path2.default.basename(filename);
575
+ const absolutePath = import_path2.default.join(process.cwd(), uploadDir, sanitizedFilename);
576
+ await (0, import_promises2.unlink)(absolutePath);
577
+ return import_server2.NextResponse.json({ success: true });
578
+ } catch (error) {
579
+ return import_server2.NextResponse.json({ error: "Failed to delete file: " + error.message }, { status: 500 });
580
+ }
581
+ };
582
+ return { GET, POST, DELETE };
583
+ }
420
584
  // Annotate the CommonJS export names for ESM import in node:
421
585
  0 && (module.exports = {
422
586
  ForbiddenError,
423
587
  PrismaCrud,
424
588
  createApiHandler,
589
+ createUploadHandler,
425
590
  parsePrismaQuery,
426
591
  sayHello
427
592
  });
package/dist/index.mjs CHANGED
@@ -88,7 +88,15 @@ function parsePrismaQuery(queryString, options = {}) {
88
88
  const rawVal = value.substring(dotIndex + 1);
89
89
  const prismaOp = OPERATOR_MAP[op];
90
90
  if (prismaOp) {
91
- whereTarget[targetKey] = { [prismaOp]: parseValue(rawVal, op) };
91
+ const parsedVal = parseValue(rawVal, op);
92
+ if (["contains", "startsWith", "endsWith"].includes(prismaOp) && typeof parsedVal === "string") {
93
+ whereTarget[targetKey] = {
94
+ [prismaOp]: parsedVal,
95
+ mode: "insensitive"
96
+ };
97
+ } else {
98
+ whereTarget[targetKey] = { [prismaOp]: parsedVal };
99
+ }
92
100
  return;
93
101
  }
94
102
  }
@@ -154,6 +162,8 @@ function parseValue(val, op) {
154
162
  }
155
163
 
156
164
  // src/core/prisma-crud.ts
165
+ import { unlink } from "fs/promises";
166
+ import { join } from "path";
157
167
  var PrismaCrud = class {
158
168
  /**
159
169
  * @param model -The Prisma model delegate (e.g., prisma.user)
@@ -256,7 +266,29 @@ var PrismaCrud = class {
256
266
  if (!this.checkPermission("update", context)) throw new ForbiddenError();
257
267
  try {
258
268
  if (schema) data = schema.parse(data);
259
- const where = this.applySecurity({ id: this.parseId(id) }, context);
269
+ const parsedId = this.parseId(id);
270
+ const where = this.applySecurity({ id: parsedId }, context);
271
+ if (this.config.imageFields && this.config.imageFields.length > 0) {
272
+ const oldRecord = await this.model.findUnique({ where });
273
+ if (oldRecord) {
274
+ for (const field of this.config.imageFields) {
275
+ const newValue = data[field];
276
+ const oldValue = oldRecord[field];
277
+ if (newValue === void 0) continue;
278
+ if (typeof oldValue === "string" && typeof newValue === "string") {
279
+ if (oldValue !== newValue) {
280
+ await this.deleteLocalFile(oldValue);
281
+ }
282
+ } else if (Array.isArray(oldValue)) {
283
+ const newArray = Array.isArray(newValue) ? newValue : [];
284
+ const removedImages = oldValue.filter((img) => !newArray.includes(img));
285
+ for (const img of removedImages) {
286
+ await this.deleteLocalFile(img);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
260
292
  if (this.config.hooks?.beforeUpdate) {
261
293
  data = await this.config.hooks.beforeUpdate(data);
262
294
  }
@@ -279,8 +311,24 @@ var PrismaCrud = class {
279
311
  async delete(id, context) {
280
312
  if (!this.checkPermission("delete", context)) throw new ForbiddenError();
281
313
  try {
282
- const where = this.applySecurity({ id: this.parseId(id) }, context);
283
314
  const parsedId = this.parseId(id);
315
+ const where = this.applySecurity({ id: parsedId }, context);
316
+ if (this.config.imageFields && this.config.imageFields.length > 0) {
317
+ const record = await this.model.findUnique({ where });
318
+ if (record) {
319
+ for (const field of this.config.imageFields) {
320
+ const value = record[field];
321
+ if (!value) continue;
322
+ if (Array.isArray(value)) {
323
+ for (const img of value) {
324
+ await this.deleteLocalFile(img);
325
+ }
326
+ } else if (typeof value === "string") {
327
+ await this.deleteLocalFile(value);
328
+ }
329
+ }
330
+ }
331
+ }
284
332
  if (this.config.hooks?.beforeDelete) {
285
333
  await this.config.hooks.beforeDelete(parsedId);
286
334
  }
@@ -294,6 +342,20 @@ var PrismaCrud = class {
294
342
  throw error;
295
343
  }
296
344
  }
345
+ /**
346
+ * Deletes a file from the filesystem if it belongs to the uploads directory.
347
+ */
348
+ async deleteLocalFile(relativeUrl) {
349
+ try {
350
+ if (!relativeUrl || relativeUrl.startsWith("http")) return;
351
+ const publicDir = this.config.publicDir || "public";
352
+ const absolutePath = join(process.cwd(), publicDir, relativeUrl);
353
+ await unlink(absolutePath);
354
+ console.log(`[underme] Deleted old file: ${absolutePath}`);
355
+ } catch (error) {
356
+ console.warn(`[underme] Failed to delete file at ${relativeUrl}:`, error);
357
+ }
358
+ }
297
359
  /**
298
360
  * Logic to detect if an ID is a number (Int) or string (UUID/CUID).
299
361
  * This allows the API to handle both `id=1` and `id=uuid-string` automatically.
@@ -387,10 +449,102 @@ function createApiHandler(crud, options = {}) {
387
449
  };
388
450
  return { GET, POST, PATCH, DELETE };
389
451
  }
452
+
453
+ // src/adapters/upload-handler.ts
454
+ import { NextResponse as NextResponse2 } from "next/server";
455
+ import { writeFile, mkdir, readdir, stat, unlink as unlink2 } from "fs/promises";
456
+ import path from "path";
457
+ import crypto from "crypto";
458
+ function createUploadHandler(config = {}) {
459
+ const uploadDir = config.uploadDir || "public/uploads";
460
+ const maxSize = config.maxSize || 5 * 1024 * 1024;
461
+ const allowedTypes = config.allowedTypes || ["image/jpeg", "image/png", "image/webp", "image/gif"];
462
+ const GET = async () => {
463
+ try {
464
+ const absoluteUploadDir = path.join(process.cwd(), uploadDir);
465
+ await mkdir(absoluteUploadDir, { recursive: true });
466
+ const files = await readdir(absoluteUploadDir);
467
+ const results = await Promise.all(
468
+ files.map(async (filename) => {
469
+ const filePath = path.join(absoluteUploadDir, filename);
470
+ const fileStat = await stat(filePath);
471
+ if (fileStat.isDirectory()) return null;
472
+ return {
473
+ url: `/${uploadDir.replace("public/", "")}/${filename}`,
474
+ name: filename,
475
+ size: fileStat.size,
476
+ atime: fileStat.atime,
477
+ mtime: fileStat.mtime
478
+ };
479
+ })
480
+ );
481
+ return NextResponse2.json(results.filter((f) => f !== null).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()));
482
+ } catch (error) {
483
+ return NextResponse2.json({ error: "Failed to list uploads: " + error.message }, { status: 500 });
484
+ }
485
+ };
486
+ const POST = async (req) => {
487
+ try {
488
+ const formData = await req.formData();
489
+ const files = formData.getAll("file");
490
+ if (!files || files.length === 0) {
491
+ return NextResponse2.json({ error: "No files uploaded" }, { status: 400 });
492
+ }
493
+ const results = [];
494
+ const absoluteUploadDir = path.join(process.cwd(), uploadDir);
495
+ await mkdir(absoluteUploadDir, { recursive: true });
496
+ for (const file of files) {
497
+ if (file.size > maxSize) {
498
+ return NextResponse2.json({
499
+ error: `File ${file.name} is too large. Max size is ${maxSize / (1024 * 1024)}MB`
500
+ }, { status: 400 });
501
+ }
502
+ if (!allowedTypes.includes(file.type)) {
503
+ return NextResponse2.json({
504
+ error: `File type ${file.type} is not allowed.`
505
+ }, { status: 400 });
506
+ }
507
+ const buffer = Buffer.from(await file.arrayBuffer());
508
+ const extension = path.extname(file.name);
509
+ const filename = `${crypto.randomUUID()}${extension}`;
510
+ const filePath = path.join(absoluteUploadDir, filename);
511
+ await writeFile(filePath, buffer);
512
+ results.push({
513
+ url: `/${uploadDir.replace("public/", "")}/${filename}`,
514
+ name: filename,
515
+ // Original filename is replaced with UUID for safety
516
+ size: file.size,
517
+ type: file.type
518
+ });
519
+ }
520
+ return NextResponse2.json(files.length === 1 ? results[0] : results);
521
+ } catch (error) {
522
+ console.error("Upload Handler Error:", error);
523
+ return NextResponse2.json({ error: "Upload failed: " + error.message }, { status: 500 });
524
+ }
525
+ };
526
+ const DELETE = async (req) => {
527
+ try {
528
+ const { searchParams } = new URL(req.url);
529
+ const filename = searchParams.get("file");
530
+ if (!filename) {
531
+ return NextResponse2.json({ error: "Filename is required" }, { status: 400 });
532
+ }
533
+ const sanitizedFilename = path.basename(filename);
534
+ const absolutePath = path.join(process.cwd(), uploadDir, sanitizedFilename);
535
+ await unlink2(absolutePath);
536
+ return NextResponse2.json({ success: true });
537
+ } catch (error) {
538
+ return NextResponse2.json({ error: "Failed to delete file: " + error.message }, { status: 500 });
539
+ }
540
+ };
541
+ return { GET, POST, DELETE };
542
+ }
390
543
  export {
391
544
  ForbiddenError,
392
545
  PrismaCrud,
393
546
  createApiHandler,
547
+ createUploadHandler,
394
548
  parsePrismaQuery,
395
549
  sayHello
396
550
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "say-under-me",
3
- "version": "1.4.0",
3
+ "version": "1.5.2",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -25,5 +25,8 @@
25
25
  "react-dom": "^19.2.4",
26
26
  "tsup": "^8.0.0",
27
27
  "typescript": "^5.0.0"
28
+ },
29
+ "dependencies": {
30
+ "say-under-me": "^1.5.0"
28
31
  }
29
32
  }