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 +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +168 -3
- package/dist/index.mjs +157 -3
- package/package.json +4 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
}
|