@airoom/nextmin-node 0.1.7 → 0.1.9
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.
|
@@ -213,21 +213,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
213
213
|
const sortRaw = String(req.query.sort ?? '').trim();
|
|
214
214
|
const sortTypeRaw = String(req.query.sortType ?? '').trim();
|
|
215
215
|
const filter = {};
|
|
216
|
-
|
|
217
|
-
const keys = (0, utils_1.splitCSV)(sortRaw);
|
|
218
|
-
const dirs = (0, utils_1.splitCSV)(sortTypeRaw);
|
|
219
|
-
const s = {};
|
|
220
|
-
keys.forEach((k, i) => {
|
|
221
|
-
if (!schema.attributes?.[k])
|
|
222
|
-
return;
|
|
223
|
-
const d = dirs[i] ?? dirs[dirs.length - 1] ?? 'desc';
|
|
224
|
-
s[k] = /^(desc|-1)$/i.test(d) ? -1 : 1;
|
|
225
|
-
});
|
|
226
|
-
if (!Object.keys(s).length)
|
|
227
|
-
s.createdAt = -1;
|
|
228
|
-
return s;
|
|
229
|
-
};
|
|
230
|
-
// SEARCH
|
|
216
|
+
// ---------- SEARCH ----------
|
|
231
217
|
const ors = [];
|
|
232
218
|
const ands = [];
|
|
233
219
|
if (q && searchKey && schema.attributes?.[searchKey]) {
|
|
@@ -277,7 +263,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
277
263
|
filter[dateKey] = range;
|
|
278
264
|
}
|
|
279
265
|
}
|
|
280
|
-
//
|
|
266
|
+
// ---------- Direct field filters from req.query ----------
|
|
281
267
|
const RESERVED = new Set([
|
|
282
268
|
'page',
|
|
283
269
|
'limit',
|
|
@@ -354,21 +340,23 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
354
340
|
if (!rdec.allow) {
|
|
355
341
|
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
356
342
|
}
|
|
357
|
-
|
|
343
|
+
// ---------- SORT (updated; type-safe; honors client; allows virtual keys) ----------
|
|
344
|
+
const baseKeys = new Set(Object.keys(schema.attributes || {}));
|
|
345
|
+
const baseSort = (0, utils_1.parseSort)(sortRaw, sortTypeRaw, baseKeys);
|
|
358
346
|
const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
|
|
359
347
|
const isUsersModel = modelNameLC === 'user' ||
|
|
360
348
|
modelNameLC === 'users' ||
|
|
361
349
|
(schema?.modelName ?? '').toLowerCase() === 'users';
|
|
362
350
|
const currentUserId = req?.user?.id;
|
|
363
|
-
// EXTENDED PATH
|
|
351
|
+
// ---------- EXTENDED PATH ----------
|
|
364
352
|
if (schema.extends) {
|
|
365
353
|
const baseName = String(schema.extends);
|
|
366
354
|
const baseLC = baseName.toLowerCase();
|
|
367
355
|
const baseSchema = ctx.getSchema(baseLC);
|
|
368
356
|
const baseModel = ctx.getModel(baseLC);
|
|
369
|
-
const
|
|
370
|
-
const { child: childFilter, base: extBaseFilter } = (0, utils_1.splitFilterForExtended)(finalFilter,
|
|
371
|
-
const { child: childSort, base: extBaseSort } = (0, utils_1.splitSortForExtended)(baseSort,
|
|
357
|
+
const baseAttrKeys = new Set(Object.keys(baseSchema.attributes || {}));
|
|
358
|
+
const { child: childFilter, base: extBaseFilter } = (0, utils_1.splitFilterForExtended)(finalFilter, baseAttrKeys);
|
|
359
|
+
const { child: childSort, base: extBaseSort } = (0, utils_1.splitSortForExtended)(baseSort, baseAttrKeys);
|
|
372
360
|
const needPrivateForHydrate = true;
|
|
373
361
|
const requiresBaseProcessing = Object.keys(extBaseFilter).length > 0 ||
|
|
374
362
|
Object.keys(extBaseSort).length > 0;
|
|
@@ -407,7 +395,26 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
407
395
|
}
|
|
408
396
|
const combinedSort = { ...extBaseSort, ...childSort };
|
|
409
397
|
if (Object.keys(combinedSort).length) {
|
|
410
|
-
merged = (
|
|
398
|
+
merged = merged.sort((a, b) => {
|
|
399
|
+
for (const k of Object.keys(combinedSort)) {
|
|
400
|
+
const dir = combinedSort[k];
|
|
401
|
+
const av = a?.[k];
|
|
402
|
+
const bv = b?.[k];
|
|
403
|
+
const ax = av instanceof Date ? +av : av;
|
|
404
|
+
const bx = bv instanceof Date ? +bv : bv;
|
|
405
|
+
if (ax == null && bx == null)
|
|
406
|
+
continue;
|
|
407
|
+
if (ax == null)
|
|
408
|
+
return 1 * dir;
|
|
409
|
+
if (bx == null)
|
|
410
|
+
return -1 * dir;
|
|
411
|
+
if (ax > bx)
|
|
412
|
+
return 1 * dir;
|
|
413
|
+
if (ax < bx)
|
|
414
|
+
return -1 * dir;
|
|
415
|
+
}
|
|
416
|
+
return 0;
|
|
417
|
+
});
|
|
411
418
|
}
|
|
412
419
|
// **dedupe**: one row per base entity
|
|
413
420
|
merged = (0, utils_1.dedupeBy)(merged, (r) => String(r.exId || r.id || r._id || ''));
|
|
@@ -423,9 +430,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
423
430
|
paged = merged.slice(start, start + limit);
|
|
424
431
|
}
|
|
425
432
|
else {
|
|
426
|
-
|
|
427
|
-
// totalRows = await (model as any).countDistinct?.('baseId', childFilter) ?? merged.length;
|
|
428
|
-
totalRows = merged.length; // fallback keeps UI consistent with deduped list
|
|
433
|
+
totalRows = merged.length;
|
|
429
434
|
paged = merged.slice(0, limit);
|
|
430
435
|
}
|
|
431
436
|
const data = rdec.exposePrivate
|
|
@@ -439,7 +444,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
439
444
|
sort: combinedSort,
|
|
440
445
|
});
|
|
441
446
|
}
|
|
442
|
-
// NON-EXTENDED
|
|
447
|
+
// ---------- NON-EXTENDED ----------
|
|
443
448
|
const sort = baseSort;
|
|
444
449
|
const totalRows = await model.count(finalFilter);
|
|
445
450
|
let rawRows = [];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { RouterCtx } from
|
|
1
|
+
import type { RouterCtx } from './ctx';
|
|
2
2
|
export declare function setupFileRoutes(ctx: RouterCtx): void;
|
|
@@ -14,23 +14,26 @@ function setupFileRoutes(ctx) {
|
|
|
14
14
|
limits: { fileSize: 50 * 1024 * 1024, files: 20 },
|
|
15
15
|
});
|
|
16
16
|
const fileAuthMiddleware = (req, res, next) => ctx.authenticateMiddleware(req, res, next);
|
|
17
|
-
|
|
17
|
+
const fileApiKeyMiddleware = (req, res, next) => ctx.apiKeyMiddleware(req, res, next);
|
|
18
|
+
ctx.router.post('/file', fileApiKeyMiddleware, upload.any(), async (req, res) => {
|
|
18
19
|
try {
|
|
19
20
|
const files = req.files ?? [];
|
|
20
21
|
if (!files.length) {
|
|
21
|
-
return res
|
|
22
|
+
return res
|
|
23
|
+
.status(400)
|
|
24
|
+
.json({ error: true, message: 'No files uploaded' });
|
|
22
25
|
}
|
|
23
26
|
const results = await Promise.all(files.map(async (f) => {
|
|
24
27
|
const folder = shortFolder();
|
|
25
28
|
const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
|
|
26
29
|
(0, filename_1.extFromMime)(f.mimetype) ??
|
|
27
|
-
|
|
30
|
+
'bin').toLowerCase();
|
|
28
31
|
const key = `${folder}/${shortUid()}.${ext}`;
|
|
29
32
|
const out = await ctx.fileStorage.upload({
|
|
30
33
|
key,
|
|
31
34
|
body: f.buffer,
|
|
32
35
|
contentType: f.mimetype,
|
|
33
|
-
metadata: { originalName: f.originalname ||
|
|
36
|
+
metadata: { originalName: f.originalname || '' },
|
|
34
37
|
});
|
|
35
38
|
return {
|
|
36
39
|
provider: out.provider,
|
|
@@ -46,38 +49,88 @@ function setupFileRoutes(ctx) {
|
|
|
46
49
|
}));
|
|
47
50
|
return res.json({
|
|
48
51
|
success: true,
|
|
49
|
-
message:
|
|
52
|
+
message: 'Files uploaded successfully',
|
|
50
53
|
data: results,
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
catch (err) {
|
|
54
|
-
return res
|
|
57
|
+
return res
|
|
58
|
+
.status(400)
|
|
59
|
+
.json({ error: true, message: err?.message ?? 'Upload failed' });
|
|
55
60
|
}
|
|
56
61
|
});
|
|
57
|
-
ctx.router.
|
|
62
|
+
ctx.router.post('/files', fileAuthMiddleware, upload.any(), async (req, res) => {
|
|
58
63
|
try {
|
|
59
|
-
const
|
|
64
|
+
const files = req.files ?? [];
|
|
65
|
+
if (!files.length) {
|
|
66
|
+
return res
|
|
67
|
+
.status(400)
|
|
68
|
+
.json({ error: true, message: 'No files uploaded' });
|
|
69
|
+
}
|
|
70
|
+
const results = await Promise.all(files.map(async (f) => {
|
|
71
|
+
const folder = shortFolder();
|
|
72
|
+
const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
|
|
73
|
+
(0, filename_1.extFromMime)(f.mimetype) ??
|
|
74
|
+
'bin').toLowerCase();
|
|
75
|
+
const key = `${folder}/${shortUid()}.${ext}`;
|
|
76
|
+
const out = await ctx.fileStorage.upload({
|
|
77
|
+
key,
|
|
78
|
+
body: f.buffer,
|
|
79
|
+
contentType: f.mimetype,
|
|
80
|
+
metadata: { originalName: f.originalname || '' },
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
provider: out.provider,
|
|
84
|
+
bucket: out.bucket,
|
|
85
|
+
key: out.key,
|
|
86
|
+
url: out.url,
|
|
87
|
+
etag: out.etag,
|
|
88
|
+
contentType: out.contentType,
|
|
89
|
+
size: out.size,
|
|
90
|
+
metadata: out.metadata,
|
|
91
|
+
originalName: f.originalname,
|
|
92
|
+
};
|
|
93
|
+
}));
|
|
94
|
+
return res.json({
|
|
95
|
+
success: true,
|
|
96
|
+
message: 'Files uploaded successfully',
|
|
97
|
+
data: results,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return res
|
|
102
|
+
.status(400)
|
|
103
|
+
.json({ error: true, message: err?.message ?? 'Upload failed' });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
ctx.router.delete('/files/:key(*)', fileAuthMiddleware, async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const key = String(req.params.key || '');
|
|
60
109
|
if (!key) {
|
|
61
|
-
return res
|
|
110
|
+
return res
|
|
111
|
+
.status(400)
|
|
112
|
+
.json({ error: true, message: 'Key is required' });
|
|
62
113
|
}
|
|
63
114
|
const { deleted } = await ctx.fileStorage.delete(key);
|
|
64
115
|
return res.json({
|
|
65
116
|
success: true,
|
|
66
|
-
message: deleted ?
|
|
117
|
+
message: deleted ? 'File deleted' : 'Delete attempted',
|
|
67
118
|
key,
|
|
68
119
|
deleted,
|
|
69
120
|
});
|
|
70
121
|
}
|
|
71
122
|
catch (err) {
|
|
72
|
-
return res
|
|
123
|
+
return res
|
|
124
|
+
.status(400)
|
|
125
|
+
.json({ error: true, message: err?.message ?? 'Delete failed' });
|
|
73
126
|
}
|
|
74
127
|
});
|
|
75
128
|
}
|
|
76
129
|
function shortFolder() {
|
|
77
130
|
const d = new Date();
|
|
78
131
|
const y = d.getFullYear();
|
|
79
|
-
const m = String(d.getMonth() + 1).padStart(2,
|
|
80
|
-
const day = String(d.getDate()).padStart(2,
|
|
132
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
133
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
81
134
|
return `uploads/${y}/${m}/${day}`;
|
|
82
135
|
}
|
|
83
136
|
function shortUid() {
|
|
@@ -2,6 +2,7 @@ import type { Request } from 'express';
|
|
|
2
2
|
export type AnyRec = Record<string, any>;
|
|
3
3
|
export type SortDir = 1 | -1;
|
|
4
4
|
export type SortSpec = Record<string, SortDir>;
|
|
5
|
+
export declare const ALLOWED_VIRTUAL_ATTRS: Set<string>;
|
|
5
6
|
export declare const isPlainObject: (v: unknown) => v is AnyRec;
|
|
6
7
|
export declare const splitCSV: (raw: string) => string[];
|
|
7
8
|
export declare function normalizeAttrType(attr: any): string;
|
|
@@ -46,16 +47,16 @@ export declare function buildPredicateForField(field: string, attr: any, raw: st
|
|
|
46
47
|
} | {
|
|
47
48
|
[field]: Date;
|
|
48
49
|
} | null;
|
|
49
|
-
export declare
|
|
50
|
-
export declare function
|
|
50
|
+
export declare const VIRTUAL_SORT_KEYS: Set<string>;
|
|
51
|
+
export declare function parseSort(sortRaw: unknown, sortTypeRaw: unknown, allowedKeys?: Set<string>): SortSpec;
|
|
52
|
+
export type ParsedQuery = {
|
|
51
53
|
limit: number;
|
|
52
54
|
page: number;
|
|
53
55
|
skip: number;
|
|
54
|
-
projection
|
|
55
|
-
|
|
56
|
-
} | undefined;
|
|
57
|
-
sort: Record<string, 1 | -1> | undefined;
|
|
56
|
+
projection?: Record<string, 1>;
|
|
57
|
+
sort: SortSpec;
|
|
58
58
|
};
|
|
59
|
+
export declare function parseQuery(req: Request, allowedKeys?: Set<string>): ParsedQuery;
|
|
59
60
|
export declare function extractIds(val: unknown): string[];
|
|
60
61
|
export declare function refInfoFromAttr(attr: any): {
|
|
61
62
|
ref: string;
|
package/dist/api/router/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dedupeBy = exports.splitCSV = exports.isPlainObject = void 0;
|
|
3
|
+
exports.dedupeBy = exports.VIRTUAL_SORT_KEYS = exports.splitCSV = exports.isPlainObject = exports.ALLOWED_VIRTUAL_ATTRS = void 0;
|
|
4
4
|
exports.normalizeAttrType = normalizeAttrType;
|
|
5
5
|
exports.toIdString = toIdString;
|
|
6
6
|
exports.splitFilterForExtended = splitFilterForExtended;
|
|
@@ -12,6 +12,12 @@ exports.parseSort = parseSort;
|
|
|
12
12
|
exports.parseQuery = parseQuery;
|
|
13
13
|
exports.extractIds = extractIds;
|
|
14
14
|
exports.refInfoFromAttr = refInfoFromAttr;
|
|
15
|
+
exports.ALLOWED_VIRTUAL_ATTRS = new Set([
|
|
16
|
+
'createdAt',
|
|
17
|
+
'updatedAt',
|
|
18
|
+
'_id',
|
|
19
|
+
'id',
|
|
20
|
+
]);
|
|
15
21
|
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
16
22
|
exports.isPlainObject = isPlainObject;
|
|
17
23
|
const splitCSV = (raw) => raw
|
|
@@ -206,35 +212,68 @@ function buildPredicateForField(field, attr, raw) {
|
|
|
206
212
|
: { [field]: raw };
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
exports.VIRTUAL_SORT_KEYS = new Set([
|
|
216
|
+
'createdAt',
|
|
217
|
+
'updatedAt',
|
|
218
|
+
'_id',
|
|
219
|
+
'id',
|
|
220
|
+
]);
|
|
221
|
+
function parseSort(sortRaw, sortTypeRaw, allowedKeys) {
|
|
222
|
+
const keys = String(sortRaw ?? '')
|
|
214
223
|
.split(',')
|
|
215
224
|
.map((s) => s.trim())
|
|
216
|
-
.filter(Boolean)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
const dirs = String(sortTypeRaw ?? '')
|
|
227
|
+
.split(',')
|
|
228
|
+
.map((s) => s.trim())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
const hasKey = (k) => (allowedKeys?.has(k) ?? false) || exports.VIRTUAL_SORT_KEYS.has(k);
|
|
231
|
+
const spec = {};
|
|
232
|
+
if (keys.length) {
|
|
233
|
+
keys.forEach((k, i) => {
|
|
234
|
+
let field = k;
|
|
235
|
+
let dir = null;
|
|
236
|
+
// compact + / - prefix
|
|
237
|
+
if (k.startsWith('-')) {
|
|
238
|
+
field = k.slice(1);
|
|
239
|
+
dir = -1;
|
|
240
|
+
}
|
|
241
|
+
else if (k.startsWith('+')) {
|
|
242
|
+
field = k.slice(1);
|
|
243
|
+
dir = 1;
|
|
244
|
+
}
|
|
245
|
+
if (dir === null) {
|
|
246
|
+
const d = dirs[i] ?? dirs[dirs.length - 1] ?? 'desc';
|
|
247
|
+
dir = /^(desc|-1)$/i.test(d)
|
|
248
|
+
? -1
|
|
249
|
+
: /^(asc|1)$/i.test(d)
|
|
250
|
+
? 1
|
|
251
|
+
: -1;
|
|
252
|
+
}
|
|
253
|
+
if (field && hasKey(field)) {
|
|
254
|
+
spec[field] = dir;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// client sent sort but none validated → honor “no default”
|
|
258
|
+
return spec;
|
|
223
259
|
}
|
|
224
|
-
|
|
260
|
+
// no sort sent → default
|
|
261
|
+
return { createdAt: -1 };
|
|
225
262
|
}
|
|
226
|
-
function parseQuery(req) {
|
|
227
|
-
const limit = Math.min(parseInt(String(req.query.limit ?? '12'), 10) || 12, 100);
|
|
228
|
-
const page = Math.max(parseInt(String(req.query.page ?? '1'), 10) || 1, 1);
|
|
263
|
+
function parseQuery(req, allowedKeys) {
|
|
264
|
+
const limit = Math.min(Number.parseInt(String(req.query.limit ?? '12'), 10) || 12, 100);
|
|
265
|
+
const page = Math.max(Number.parseInt(String(req.query.page ?? '1'), 10) || 1, 1);
|
|
229
266
|
const skip = (page - 1) * limit;
|
|
230
267
|
const fields = String(req.query.fields ?? '')
|
|
231
268
|
.split(',')
|
|
232
269
|
.map((s) => s.trim())
|
|
233
270
|
.filter(Boolean);
|
|
234
|
-
const projection = fields.length
|
|
271
|
+
const projection = fields.length > 0
|
|
235
272
|
? Object.fromEntries(fields.map((f) => [f, 1]))
|
|
236
273
|
: undefined;
|
|
237
|
-
|
|
274
|
+
// NOTE: do NOT force a fallback like '-createdAt' here;
|
|
275
|
+
// parseSort will apply { createdAt: -1 } only if no sort is provided.
|
|
276
|
+
const sort = parseSort(req.query.sort, req.query.sortType, allowedKeys);
|
|
238
277
|
return { limit, page, skip, projection, sort };
|
|
239
278
|
}
|
|
240
279
|
function extractIds(val) {
|