say-under-me 1.3.1 → 1.5.0
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 +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +174 -9
- package/dist/index.mjs +163 -9
- package/package.json +1 -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
|
}
|
|
@@ -56,6 +69,7 @@ declare function sayHello({ firstName, lastName, age, }: sayHelloProps): void;
|
|
|
56
69
|
type PrismaQuery = {
|
|
57
70
|
where?: Record<string, any>;
|
|
58
71
|
select?: Record<string, any>;
|
|
72
|
+
include?: Record<string, any>;
|
|
59
73
|
orderBy?: Record<string, any> | Record<string, any>[];
|
|
60
74
|
take?: number;
|
|
61
75
|
skip?: number;
|
|
@@ -107,6 +121,10 @@ declare class PrismaCrud<T> {
|
|
|
107
121
|
* Deletes a record by ID with optional lifecycle hooks.
|
|
108
122
|
*/
|
|
109
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;
|
|
110
128
|
/**
|
|
111
129
|
* Logic to detect if an ID is a number (Int) or string (UUID/CUID).
|
|
112
130
|
* This allows the API to handle both `id=1` and `id=uuid-string` automatically.
|
|
@@ -132,4 +150,23 @@ declare function createApiHandler<T>(crud: PrismaCrud<T>, options?: {
|
|
|
132
150
|
DELETE: (req: NextRequest, context: any) => Promise<NextResponse<unknown>>;
|
|
133
151
|
};
|
|
134
152
|
|
|
135
|
-
|
|
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
|
}
|
|
@@ -56,6 +69,7 @@ declare function sayHello({ firstName, lastName, age, }: sayHelloProps): void;
|
|
|
56
69
|
type PrismaQuery = {
|
|
57
70
|
where?: Record<string, any>;
|
|
58
71
|
select?: Record<string, any>;
|
|
72
|
+
include?: Record<string, any>;
|
|
59
73
|
orderBy?: Record<string, any> | Record<string, any>[];
|
|
60
74
|
take?: number;
|
|
61
75
|
skip?: number;
|
|
@@ -107,6 +121,10 @@ declare class PrismaCrud<T> {
|
|
|
107
121
|
* Deletes a record by ID with optional lifecycle hooks.
|
|
108
122
|
*/
|
|
109
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;
|
|
110
128
|
/**
|
|
111
129
|
* Logic to detect if an ID is a number (Int) or string (UUID/CUID).
|
|
112
130
|
* This allows the API to handle both `id=1` and `id=uuid-string` automatically.
|
|
@@ -132,4 +150,23 @@ declare function createApiHandler<T>(crud: PrismaCrud<T>, options?: {
|
|
|
132
150
|
DELETE: (req: NextRequest, context: any) => Promise<NextResponse<unknown>>;
|
|
133
151
|
};
|
|
134
152
|
|
|
135
|
-
|
|
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
|
});
|
|
@@ -96,7 +107,9 @@ function parsePrismaQuery(queryString, options = {}) {
|
|
|
96
107
|
return;
|
|
97
108
|
}
|
|
98
109
|
if (key === "select") {
|
|
99
|
-
|
|
110
|
+
const parsed = parseSelect(value, allowedFields);
|
|
111
|
+
if (parsed.select) result.select = parsed.select;
|
|
112
|
+
if (parsed.include) result.include = parsed.include;
|
|
100
113
|
return;
|
|
101
114
|
}
|
|
102
115
|
if (key === "order") return;
|
|
@@ -128,7 +141,7 @@ function parsePrismaQuery(queryString, options = {}) {
|
|
|
128
141
|
}
|
|
129
142
|
function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
130
143
|
const result = {};
|
|
131
|
-
if (!selectStr) return result;
|
|
144
|
+
if (!selectStr) return { select: result };
|
|
132
145
|
let depth = 0;
|
|
133
146
|
let current = "";
|
|
134
147
|
const fields = [];
|
|
@@ -144,16 +157,18 @@ function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
if (current) fields.push(current.trim());
|
|
160
|
+
const hasWildcard = fields.includes("*");
|
|
147
161
|
fields.forEach((field) => {
|
|
162
|
+
if (field === "*") return;
|
|
148
163
|
if (field.includes("(")) {
|
|
149
164
|
const start = field.indexOf("(");
|
|
150
165
|
const end = field.lastIndexOf(")");
|
|
151
166
|
const key = field.substring(0, start).trim();
|
|
152
167
|
const nestedContent = field.substring(start + 1, end).trim();
|
|
153
168
|
const currentPath = parentPath ? `${parentPath}.${key}` : key;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
const nested = parseSelect(nestedContent, allowedFields, currentPath);
|
|
170
|
+
const nestedValue = nested.select ? { select: nested.select } : nested.include ? { include: nested.include } : true;
|
|
171
|
+
result[key] = nestedValue;
|
|
157
172
|
} else {
|
|
158
173
|
const currentPath = parentPath ? `${parentPath}.${field}` : field;
|
|
159
174
|
if (!allowedFields || allowedFields.includes(currentPath)) {
|
|
@@ -163,7 +178,10 @@ function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
|
163
178
|
}
|
|
164
179
|
}
|
|
165
180
|
});
|
|
166
|
-
|
|
181
|
+
if (hasWildcard) {
|
|
182
|
+
return { include: Object.keys(result).length ? result : void 0 };
|
|
183
|
+
}
|
|
184
|
+
return { select: result };
|
|
167
185
|
}
|
|
168
186
|
function parseValue(val, op) {
|
|
169
187
|
if (val === "true") return true;
|
|
@@ -177,6 +195,8 @@ function parseValue(val, op) {
|
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
// src/core/prisma-crud.ts
|
|
198
|
+
var import_promises = require("fs/promises");
|
|
199
|
+
var import_path = require("path");
|
|
180
200
|
var PrismaCrud = class {
|
|
181
201
|
/**
|
|
182
202
|
* @param model -The Prisma model delegate (e.g., prisma.user)
|
|
@@ -242,7 +262,8 @@ var PrismaCrud = class {
|
|
|
242
262
|
const where = this.applySecurity({ id: this.parseId(id) }, context);
|
|
243
263
|
const record = await this.model.findUnique({
|
|
244
264
|
where,
|
|
245
|
-
...query.select ? { select: query.select } : {}
|
|
265
|
+
...query.select ? { select: query.select } : {},
|
|
266
|
+
...query.include ? { include: query.include } : {}
|
|
246
267
|
});
|
|
247
268
|
if (!record) throw new Error("Record not found");
|
|
248
269
|
return record;
|
|
@@ -278,7 +299,29 @@ var PrismaCrud = class {
|
|
|
278
299
|
if (!this.checkPermission("update", context)) throw new ForbiddenError();
|
|
279
300
|
try {
|
|
280
301
|
if (schema) data = schema.parse(data);
|
|
281
|
-
const
|
|
302
|
+
const parsedId = this.parseId(id);
|
|
303
|
+
const where = this.applySecurity({ id: parsedId }, context);
|
|
304
|
+
if (this.config.imageFields && this.config.imageFields.length > 0) {
|
|
305
|
+
const oldRecord = await this.model.findUnique({ where });
|
|
306
|
+
if (oldRecord) {
|
|
307
|
+
for (const field of this.config.imageFields) {
|
|
308
|
+
const newValue = data[field];
|
|
309
|
+
const oldValue = oldRecord[field];
|
|
310
|
+
if (newValue === void 0) continue;
|
|
311
|
+
if (typeof oldValue === "string" && typeof newValue === "string") {
|
|
312
|
+
if (oldValue !== newValue) {
|
|
313
|
+
await this.deleteLocalFile(oldValue);
|
|
314
|
+
}
|
|
315
|
+
} else if (Array.isArray(oldValue)) {
|
|
316
|
+
const newArray = Array.isArray(newValue) ? newValue : [];
|
|
317
|
+
const removedImages = oldValue.filter((img) => !newArray.includes(img));
|
|
318
|
+
for (const img of removedImages) {
|
|
319
|
+
await this.deleteLocalFile(img);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
282
325
|
if (this.config.hooks?.beforeUpdate) {
|
|
283
326
|
data = await this.config.hooks.beforeUpdate(data);
|
|
284
327
|
}
|
|
@@ -301,8 +344,24 @@ var PrismaCrud = class {
|
|
|
301
344
|
async delete(id, context) {
|
|
302
345
|
if (!this.checkPermission("delete", context)) throw new ForbiddenError();
|
|
303
346
|
try {
|
|
304
|
-
const where = this.applySecurity({ id: this.parseId(id) }, context);
|
|
305
347
|
const parsedId = this.parseId(id);
|
|
348
|
+
const where = this.applySecurity({ id: parsedId }, context);
|
|
349
|
+
if (this.config.imageFields && this.config.imageFields.length > 0) {
|
|
350
|
+
const record = await this.model.findUnique({ where });
|
|
351
|
+
if (record) {
|
|
352
|
+
for (const field of this.config.imageFields) {
|
|
353
|
+
const value = record[field];
|
|
354
|
+
if (!value) continue;
|
|
355
|
+
if (Array.isArray(value)) {
|
|
356
|
+
for (const img of value) {
|
|
357
|
+
await this.deleteLocalFile(img);
|
|
358
|
+
}
|
|
359
|
+
} else if (typeof value === "string") {
|
|
360
|
+
await this.deleteLocalFile(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
306
365
|
if (this.config.hooks?.beforeDelete) {
|
|
307
366
|
await this.config.hooks.beforeDelete(parsedId);
|
|
308
367
|
}
|
|
@@ -316,6 +375,20 @@ var PrismaCrud = class {
|
|
|
316
375
|
throw error;
|
|
317
376
|
}
|
|
318
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Deletes a file from the filesystem if it belongs to the uploads directory.
|
|
380
|
+
*/
|
|
381
|
+
async deleteLocalFile(relativeUrl) {
|
|
382
|
+
try {
|
|
383
|
+
if (!relativeUrl || relativeUrl.startsWith("http")) return;
|
|
384
|
+
const publicDir = this.config.publicDir || "public";
|
|
385
|
+
const absolutePath = (0, import_path.join)(process.cwd(), publicDir, relativeUrl);
|
|
386
|
+
await (0, import_promises.unlink)(absolutePath);
|
|
387
|
+
console.log(`[underme] Deleted old file: ${absolutePath}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn(`[underme] Failed to delete file at ${relativeUrl}:`, error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
319
392
|
/**
|
|
320
393
|
* Logic to detect if an ID is a number (Int) or string (UUID/CUID).
|
|
321
394
|
* This allows the API to handle both `id=1` and `id=uuid-string` automatically.
|
|
@@ -409,11 +482,103 @@ function createApiHandler(crud, options = {}) {
|
|
|
409
482
|
};
|
|
410
483
|
return { GET, POST, PATCH, DELETE };
|
|
411
484
|
}
|
|
485
|
+
|
|
486
|
+
// src/adapters/upload-handler.ts
|
|
487
|
+
var import_server2 = require("next/server");
|
|
488
|
+
var import_promises2 = require("fs/promises");
|
|
489
|
+
var import_path2 = __toESM(require("path"));
|
|
490
|
+
var import_crypto = __toESM(require("crypto"));
|
|
491
|
+
function createUploadHandler(config = {}) {
|
|
492
|
+
const uploadDir = config.uploadDir || "public/uploads";
|
|
493
|
+
const maxSize = config.maxSize || 5 * 1024 * 1024;
|
|
494
|
+
const allowedTypes = config.allowedTypes || ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
495
|
+
const GET = async () => {
|
|
496
|
+
try {
|
|
497
|
+
const absoluteUploadDir = import_path2.default.join(process.cwd(), uploadDir);
|
|
498
|
+
await (0, import_promises2.mkdir)(absoluteUploadDir, { recursive: true });
|
|
499
|
+
const files = await (0, import_promises2.readdir)(absoluteUploadDir);
|
|
500
|
+
const results = await Promise.all(
|
|
501
|
+
files.map(async (filename) => {
|
|
502
|
+
const filePath = import_path2.default.join(absoluteUploadDir, filename);
|
|
503
|
+
const fileStat = await (0, import_promises2.stat)(filePath);
|
|
504
|
+
if (fileStat.isDirectory()) return null;
|
|
505
|
+
return {
|
|
506
|
+
url: `/${uploadDir.replace("public/", "")}/${filename}`,
|
|
507
|
+
name: filename,
|
|
508
|
+
size: fileStat.size,
|
|
509
|
+
atime: fileStat.atime,
|
|
510
|
+
mtime: fileStat.mtime
|
|
511
|
+
};
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
return import_server2.NextResponse.json(results.filter((f) => f !== null).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()));
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return import_server2.NextResponse.json({ error: "Failed to list uploads: " + error.message }, { status: 500 });
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const POST = async (req) => {
|
|
520
|
+
try {
|
|
521
|
+
const formData = await req.formData();
|
|
522
|
+
const files = formData.getAll("file");
|
|
523
|
+
if (!files || files.length === 0) {
|
|
524
|
+
return import_server2.NextResponse.json({ error: "No files uploaded" }, { status: 400 });
|
|
525
|
+
}
|
|
526
|
+
const results = [];
|
|
527
|
+
const absoluteUploadDir = import_path2.default.join(process.cwd(), uploadDir);
|
|
528
|
+
await (0, import_promises2.mkdir)(absoluteUploadDir, { recursive: true });
|
|
529
|
+
for (const file of files) {
|
|
530
|
+
if (file.size > maxSize) {
|
|
531
|
+
return import_server2.NextResponse.json({
|
|
532
|
+
error: `File ${file.name} is too large. Max size is ${maxSize / (1024 * 1024)}MB`
|
|
533
|
+
}, { status: 400 });
|
|
534
|
+
}
|
|
535
|
+
if (!allowedTypes.includes(file.type)) {
|
|
536
|
+
return import_server2.NextResponse.json({
|
|
537
|
+
error: `File type ${file.type} is not allowed.`
|
|
538
|
+
}, { status: 400 });
|
|
539
|
+
}
|
|
540
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
541
|
+
const extension = import_path2.default.extname(file.name);
|
|
542
|
+
const filename = `${import_crypto.default.randomUUID()}${extension}`;
|
|
543
|
+
const filePath = import_path2.default.join(absoluteUploadDir, filename);
|
|
544
|
+
await (0, import_promises2.writeFile)(filePath, buffer);
|
|
545
|
+
results.push({
|
|
546
|
+
url: `/${uploadDir.replace("public/", "")}/${filename}`,
|
|
547
|
+
name: filename,
|
|
548
|
+
// Original filename is replaced with UUID for safety
|
|
549
|
+
size: file.size,
|
|
550
|
+
type: file.type
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return import_server2.NextResponse.json(files.length === 1 ? results[0] : results);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.error("Upload Handler Error:", error);
|
|
556
|
+
return import_server2.NextResponse.json({ error: "Upload failed: " + error.message }, { status: 500 });
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const DELETE = async (req) => {
|
|
560
|
+
try {
|
|
561
|
+
const { searchParams } = new URL(req.url);
|
|
562
|
+
const filename = searchParams.get("file");
|
|
563
|
+
if (!filename) {
|
|
564
|
+
return import_server2.NextResponse.json({ error: "Filename is required" }, { status: 400 });
|
|
565
|
+
}
|
|
566
|
+
const sanitizedFilename = import_path2.default.basename(filename);
|
|
567
|
+
const absolutePath = import_path2.default.join(process.cwd(), uploadDir, sanitizedFilename);
|
|
568
|
+
await (0, import_promises2.unlink)(absolutePath);
|
|
569
|
+
return import_server2.NextResponse.json({ success: true });
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return import_server2.NextResponse.json({ error: "Failed to delete file: " + error.message }, { status: 500 });
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
return { GET, POST, DELETE };
|
|
575
|
+
}
|
|
412
576
|
// Annotate the CommonJS export names for ESM import in node:
|
|
413
577
|
0 && (module.exports = {
|
|
414
578
|
ForbiddenError,
|
|
415
579
|
PrismaCrud,
|
|
416
580
|
createApiHandler,
|
|
581
|
+
createUploadHandler,
|
|
417
582
|
parsePrismaQuery,
|
|
418
583
|
sayHello
|
|
419
584
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -66,7 +66,9 @@ function parsePrismaQuery(queryString, options = {}) {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
if (key === "select") {
|
|
69
|
-
|
|
69
|
+
const parsed = parseSelect(value, allowedFields);
|
|
70
|
+
if (parsed.select) result.select = parsed.select;
|
|
71
|
+
if (parsed.include) result.include = parsed.include;
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
if (key === "order") return;
|
|
@@ -98,7 +100,7 @@ function parsePrismaQuery(queryString, options = {}) {
|
|
|
98
100
|
}
|
|
99
101
|
function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
100
102
|
const result = {};
|
|
101
|
-
if (!selectStr) return result;
|
|
103
|
+
if (!selectStr) return { select: result };
|
|
102
104
|
let depth = 0;
|
|
103
105
|
let current = "";
|
|
104
106
|
const fields = [];
|
|
@@ -114,16 +116,18 @@ function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
if (current) fields.push(current.trim());
|
|
119
|
+
const hasWildcard = fields.includes("*");
|
|
117
120
|
fields.forEach((field) => {
|
|
121
|
+
if (field === "*") return;
|
|
118
122
|
if (field.includes("(")) {
|
|
119
123
|
const start = field.indexOf("(");
|
|
120
124
|
const end = field.lastIndexOf(")");
|
|
121
125
|
const key = field.substring(0, start).trim();
|
|
122
126
|
const nestedContent = field.substring(start + 1, end).trim();
|
|
123
127
|
const currentPath = parentPath ? `${parentPath}.${key}` : key;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
const nested = parseSelect(nestedContent, allowedFields, currentPath);
|
|
129
|
+
const nestedValue = nested.select ? { select: nested.select } : nested.include ? { include: nested.include } : true;
|
|
130
|
+
result[key] = nestedValue;
|
|
127
131
|
} else {
|
|
128
132
|
const currentPath = parentPath ? `${parentPath}.${field}` : field;
|
|
129
133
|
if (!allowedFields || allowedFields.includes(currentPath)) {
|
|
@@ -133,7 +137,10 @@ function parseSelect(selectStr, allowedFields, parentPath = "") {
|
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
});
|
|
136
|
-
|
|
140
|
+
if (hasWildcard) {
|
|
141
|
+
return { include: Object.keys(result).length ? result : void 0 };
|
|
142
|
+
}
|
|
143
|
+
return { select: result };
|
|
137
144
|
}
|
|
138
145
|
function parseValue(val, op) {
|
|
139
146
|
if (val === "true") return true;
|
|
@@ -147,6 +154,8 @@ function parseValue(val, op) {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
// src/core/prisma-crud.ts
|
|
157
|
+
import { unlink } from "fs/promises";
|
|
158
|
+
import { join } from "path";
|
|
150
159
|
var PrismaCrud = class {
|
|
151
160
|
/**
|
|
152
161
|
* @param model -The Prisma model delegate (e.g., prisma.user)
|
|
@@ -212,7 +221,8 @@ var PrismaCrud = class {
|
|
|
212
221
|
const where = this.applySecurity({ id: this.parseId(id) }, context);
|
|
213
222
|
const record = await this.model.findUnique({
|
|
214
223
|
where,
|
|
215
|
-
...query.select ? { select: query.select } : {}
|
|
224
|
+
...query.select ? { select: query.select } : {},
|
|
225
|
+
...query.include ? { include: query.include } : {}
|
|
216
226
|
});
|
|
217
227
|
if (!record) throw new Error("Record not found");
|
|
218
228
|
return record;
|
|
@@ -248,7 +258,29 @@ var PrismaCrud = class {
|
|
|
248
258
|
if (!this.checkPermission("update", context)) throw new ForbiddenError();
|
|
249
259
|
try {
|
|
250
260
|
if (schema) data = schema.parse(data);
|
|
251
|
-
const
|
|
261
|
+
const parsedId = this.parseId(id);
|
|
262
|
+
const where = this.applySecurity({ id: parsedId }, context);
|
|
263
|
+
if (this.config.imageFields && this.config.imageFields.length > 0) {
|
|
264
|
+
const oldRecord = await this.model.findUnique({ where });
|
|
265
|
+
if (oldRecord) {
|
|
266
|
+
for (const field of this.config.imageFields) {
|
|
267
|
+
const newValue = data[field];
|
|
268
|
+
const oldValue = oldRecord[field];
|
|
269
|
+
if (newValue === void 0) continue;
|
|
270
|
+
if (typeof oldValue === "string" && typeof newValue === "string") {
|
|
271
|
+
if (oldValue !== newValue) {
|
|
272
|
+
await this.deleteLocalFile(oldValue);
|
|
273
|
+
}
|
|
274
|
+
} else if (Array.isArray(oldValue)) {
|
|
275
|
+
const newArray = Array.isArray(newValue) ? newValue : [];
|
|
276
|
+
const removedImages = oldValue.filter((img) => !newArray.includes(img));
|
|
277
|
+
for (const img of removedImages) {
|
|
278
|
+
await this.deleteLocalFile(img);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
252
284
|
if (this.config.hooks?.beforeUpdate) {
|
|
253
285
|
data = await this.config.hooks.beforeUpdate(data);
|
|
254
286
|
}
|
|
@@ -271,8 +303,24 @@ var PrismaCrud = class {
|
|
|
271
303
|
async delete(id, context) {
|
|
272
304
|
if (!this.checkPermission("delete", context)) throw new ForbiddenError();
|
|
273
305
|
try {
|
|
274
|
-
const where = this.applySecurity({ id: this.parseId(id) }, context);
|
|
275
306
|
const parsedId = this.parseId(id);
|
|
307
|
+
const where = this.applySecurity({ id: parsedId }, context);
|
|
308
|
+
if (this.config.imageFields && this.config.imageFields.length > 0) {
|
|
309
|
+
const record = await this.model.findUnique({ where });
|
|
310
|
+
if (record) {
|
|
311
|
+
for (const field of this.config.imageFields) {
|
|
312
|
+
const value = record[field];
|
|
313
|
+
if (!value) continue;
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
for (const img of value) {
|
|
316
|
+
await this.deleteLocalFile(img);
|
|
317
|
+
}
|
|
318
|
+
} else if (typeof value === "string") {
|
|
319
|
+
await this.deleteLocalFile(value);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
276
324
|
if (this.config.hooks?.beforeDelete) {
|
|
277
325
|
await this.config.hooks.beforeDelete(parsedId);
|
|
278
326
|
}
|
|
@@ -286,6 +334,20 @@ var PrismaCrud = class {
|
|
|
286
334
|
throw error;
|
|
287
335
|
}
|
|
288
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Deletes a file from the filesystem if it belongs to the uploads directory.
|
|
339
|
+
*/
|
|
340
|
+
async deleteLocalFile(relativeUrl) {
|
|
341
|
+
try {
|
|
342
|
+
if (!relativeUrl || relativeUrl.startsWith("http")) return;
|
|
343
|
+
const publicDir = this.config.publicDir || "public";
|
|
344
|
+
const absolutePath = join(process.cwd(), publicDir, relativeUrl);
|
|
345
|
+
await unlink(absolutePath);
|
|
346
|
+
console.log(`[underme] Deleted old file: ${absolutePath}`);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn(`[underme] Failed to delete file at ${relativeUrl}:`, error);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
289
351
|
/**
|
|
290
352
|
* Logic to detect if an ID is a number (Int) or string (UUID/CUID).
|
|
291
353
|
* This allows the API to handle both `id=1` and `id=uuid-string` automatically.
|
|
@@ -379,10 +441,102 @@ function createApiHandler(crud, options = {}) {
|
|
|
379
441
|
};
|
|
380
442
|
return { GET, POST, PATCH, DELETE };
|
|
381
443
|
}
|
|
444
|
+
|
|
445
|
+
// src/adapters/upload-handler.ts
|
|
446
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
447
|
+
import { writeFile, mkdir, readdir, stat, unlink as unlink2 } from "fs/promises";
|
|
448
|
+
import path from "path";
|
|
449
|
+
import crypto from "crypto";
|
|
450
|
+
function createUploadHandler(config = {}) {
|
|
451
|
+
const uploadDir = config.uploadDir || "public/uploads";
|
|
452
|
+
const maxSize = config.maxSize || 5 * 1024 * 1024;
|
|
453
|
+
const allowedTypes = config.allowedTypes || ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
454
|
+
const GET = async () => {
|
|
455
|
+
try {
|
|
456
|
+
const absoluteUploadDir = path.join(process.cwd(), uploadDir);
|
|
457
|
+
await mkdir(absoluteUploadDir, { recursive: true });
|
|
458
|
+
const files = await readdir(absoluteUploadDir);
|
|
459
|
+
const results = await Promise.all(
|
|
460
|
+
files.map(async (filename) => {
|
|
461
|
+
const filePath = path.join(absoluteUploadDir, filename);
|
|
462
|
+
const fileStat = await stat(filePath);
|
|
463
|
+
if (fileStat.isDirectory()) return null;
|
|
464
|
+
return {
|
|
465
|
+
url: `/${uploadDir.replace("public/", "")}/${filename}`,
|
|
466
|
+
name: filename,
|
|
467
|
+
size: fileStat.size,
|
|
468
|
+
atime: fileStat.atime,
|
|
469
|
+
mtime: fileStat.mtime
|
|
470
|
+
};
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
return NextResponse2.json(results.filter((f) => f !== null).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()));
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return NextResponse2.json({ error: "Failed to list uploads: " + error.message }, { status: 500 });
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const POST = async (req) => {
|
|
479
|
+
try {
|
|
480
|
+
const formData = await req.formData();
|
|
481
|
+
const files = formData.getAll("file");
|
|
482
|
+
if (!files || files.length === 0) {
|
|
483
|
+
return NextResponse2.json({ error: "No files uploaded" }, { status: 400 });
|
|
484
|
+
}
|
|
485
|
+
const results = [];
|
|
486
|
+
const absoluteUploadDir = path.join(process.cwd(), uploadDir);
|
|
487
|
+
await mkdir(absoluteUploadDir, { recursive: true });
|
|
488
|
+
for (const file of files) {
|
|
489
|
+
if (file.size > maxSize) {
|
|
490
|
+
return NextResponse2.json({
|
|
491
|
+
error: `File ${file.name} is too large. Max size is ${maxSize / (1024 * 1024)}MB`
|
|
492
|
+
}, { status: 400 });
|
|
493
|
+
}
|
|
494
|
+
if (!allowedTypes.includes(file.type)) {
|
|
495
|
+
return NextResponse2.json({
|
|
496
|
+
error: `File type ${file.type} is not allowed.`
|
|
497
|
+
}, { status: 400 });
|
|
498
|
+
}
|
|
499
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
500
|
+
const extension = path.extname(file.name);
|
|
501
|
+
const filename = `${crypto.randomUUID()}${extension}`;
|
|
502
|
+
const filePath = path.join(absoluteUploadDir, filename);
|
|
503
|
+
await writeFile(filePath, buffer);
|
|
504
|
+
results.push({
|
|
505
|
+
url: `/${uploadDir.replace("public/", "")}/${filename}`,
|
|
506
|
+
name: filename,
|
|
507
|
+
// Original filename is replaced with UUID for safety
|
|
508
|
+
size: file.size,
|
|
509
|
+
type: file.type
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return NextResponse2.json(files.length === 1 ? results[0] : results);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.error("Upload Handler Error:", error);
|
|
515
|
+
return NextResponse2.json({ error: "Upload failed: " + error.message }, { status: 500 });
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const DELETE = async (req) => {
|
|
519
|
+
try {
|
|
520
|
+
const { searchParams } = new URL(req.url);
|
|
521
|
+
const filename = searchParams.get("file");
|
|
522
|
+
if (!filename) {
|
|
523
|
+
return NextResponse2.json({ error: "Filename is required" }, { status: 400 });
|
|
524
|
+
}
|
|
525
|
+
const sanitizedFilename = path.basename(filename);
|
|
526
|
+
const absolutePath = path.join(process.cwd(), uploadDir, sanitizedFilename);
|
|
527
|
+
await unlink2(absolutePath);
|
|
528
|
+
return NextResponse2.json({ success: true });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
return NextResponse2.json({ error: "Failed to delete file: " + error.message }, { status: 500 });
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
return { GET, POST, DELETE };
|
|
534
|
+
}
|
|
382
535
|
export {
|
|
383
536
|
ForbiddenError,
|
|
384
537
|
PrismaCrud,
|
|
385
538
|
createApiHandler,
|
|
539
|
+
createUploadHandler,
|
|
386
540
|
parsePrismaQuery,
|
|
387
541
|
sayHello
|
|
388
542
|
};
|