@airoom/nextmin-node 1.4.6 → 2.0.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/README.md +48 -5
- package/dist/api/apiRouter.d.ts +10 -1
- package/dist/api/apiRouter.js +83 -22
- package/dist/api/router/mountBatchRoutes.d.ts +2 -0
- package/dist/api/router/mountBatchRoutes.js +90 -0
- package/dist/api/router/mountCrudRoutes.js +218 -223
- package/dist/api/router/mountFindRoutes.js +2 -49
- package/dist/api/router/mountSearchRoutes.js +10 -52
- package/dist/api/router/mountSearchRoutes_extended.js +7 -48
- package/dist/api/router/setupFileRoutes.js +41 -0
- package/dist/api/router/utils.d.ts +2 -0
- package/dist/api/router/utils.js +37 -8
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +8 -0
- package/dist/database/NMAdapter.d.ts +46 -0
- package/dist/database/NMAdapter.js +1243 -0
- package/dist/database/QueryEngine.d.ts +14 -0
- package/dist/database/QueryEngine.js +215 -0
- package/dist/database/utils.d.ts +2 -0
- package/dist/database/utils.js +21 -0
- package/dist/files/FileStorageAdapter.d.ts +5 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +5 -0
- package/dist/files/LocalFileStorageAdapter.js +14 -0
- package/dist/files/S3FileStorageAdapter.d.ts +5 -0
- package/dist/files/S3FileStorageAdapter.js +14 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +31 -2
- package/dist/models/BaseModel.js +33 -5
- package/dist/policy/authorize.js +95 -38
- package/dist/schemas/Users.json +66 -30
- package/dist/services/AggregateService.d.ts +8 -0
- package/dist/services/AggregateService.js +87 -0
- package/dist/services/RealtimeService.d.ts +20 -0
- package/dist/services/RealtimeService.js +93 -0
- package/dist/services/SchemaService.d.ts +3 -0
- package/dist/services/SchemaService.js +6 -2
- package/dist/utils/DefaultDataInitializer.js +10 -2
- package/dist/utils/Events.d.ts +34 -0
- package/dist/utils/Events.js +55 -0
- package/dist/utils/Logger.d.ts +2 -1
- package/dist/utils/Logger.js +20 -9
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +6 -1
- package/dist/utils/SchemaLoader.js +49 -5
- package/package.json +19 -4
- package/dist/database/InMemoryAdapter.d.ts +0 -15
- package/dist/database/InMemoryAdapter.js +0 -71
- package/dist/database/MongoAdapter.d.ts +0 -52
- package/dist/database/MongoAdapter.js +0 -410
|
@@ -83,31 +83,7 @@ function mountFindRoutes(ctx) {
|
|
|
83
83
|
else {
|
|
84
84
|
items = await ctx.getModel(targetLC).read({ id: { $in: ids } }, limit, skip, !!tDec.exposePrivate);
|
|
85
85
|
}
|
|
86
|
-
// extended hydration
|
|
87
|
-
if (targetSchema.extends && Array.isArray(items) && items.length) {
|
|
88
|
-
const baseName = String(targetSchema.extends);
|
|
89
|
-
const baseLC = baseName.toLowerCase();
|
|
90
|
-
const baseModel = ctx.getModel(baseLC);
|
|
91
|
-
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
92
|
-
if (baseIds.length) {
|
|
93
|
-
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
94
|
-
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
95
|
-
items = items.map((it) => {
|
|
96
|
-
const bid = String(it?.baseId || "");
|
|
97
|
-
const b = bid ? baseMap.get(bid) : null;
|
|
98
|
-
const merged = b ? { ...b, ...it } : { ...it };
|
|
99
|
-
delete merged.baseId;
|
|
100
|
-
return merged;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
items = items.map((it) => {
|
|
105
|
-
const copy = { ...it };
|
|
106
|
-
delete copy.baseId;
|
|
107
|
-
return copy;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
86
|
+
// extended hydration is handled by adapter.read/findMany
|
|
111
87
|
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
112
88
|
const totalRows = ids.length;
|
|
113
89
|
return res.status(200).json({
|
|
@@ -163,30 +139,7 @@ function mountFindRoutes(ctx) {
|
|
|
163
139
|
totalRows = all.length;
|
|
164
140
|
items = all.slice(skip, skip + limit);
|
|
165
141
|
}
|
|
166
|
-
|
|
167
|
-
const baseName = String(targetSchema.extends);
|
|
168
|
-
const baseLC = baseName.toLowerCase();
|
|
169
|
-
const baseModel = ctx.getModel(baseLC);
|
|
170
|
-
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
171
|
-
if (baseIds.length) {
|
|
172
|
-
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
173
|
-
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
174
|
-
items = items.map((it) => {
|
|
175
|
-
const bid = String(it?.baseId || "");
|
|
176
|
-
const b = bid ? baseMap.get(bid) : null;
|
|
177
|
-
const merged = b ? { ...b, ...it } : { ...it };
|
|
178
|
-
delete merged.baseId;
|
|
179
|
-
return merged;
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
items = items.map((it) => {
|
|
184
|
-
const copy = { ...it };
|
|
185
|
-
delete copy.baseId;
|
|
186
|
-
return copy;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
142
|
+
// extended hydration is handled by adapter.read/findMany
|
|
190
143
|
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
191
144
|
return res.status(200).json({
|
|
192
145
|
success: true,
|
|
@@ -18,6 +18,7 @@ function mountSearchRoutes(ctx) {
|
|
|
18
18
|
const perModelLimit = typeof body.limit === 'number' && body.limit > 0
|
|
19
19
|
? Math.floor(body.limit)
|
|
20
20
|
: 10;
|
|
21
|
+
const searchType = body.searchType === 'includes' ? 'includes' : 'starts';
|
|
21
22
|
if (!text || text.length < 2) {
|
|
22
23
|
return res
|
|
23
24
|
.status(400)
|
|
@@ -62,8 +63,8 @@ function mountSearchRoutes(ctx) {
|
|
|
62
63
|
const buildOrFilter = (flds) => {
|
|
63
64
|
const orClauses = [];
|
|
64
65
|
for (const f of flds) {
|
|
65
|
-
|
|
66
|
-
orClauses.push({ [f]: { $regex:
|
|
66
|
+
const pattern = searchType === 'includes' ? textEsc : `^${textEsc}`;
|
|
67
|
+
orClauses.push({ [f]: { $regex: pattern, $options: 'i' } });
|
|
67
68
|
}
|
|
68
69
|
return { $or: orClauses };
|
|
69
70
|
};
|
|
@@ -126,56 +127,13 @@ function mountSearchRoutes(ctx) {
|
|
|
126
127
|
return it;
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
|
-
// If model extends Users,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const baseUsers = await userModel.read({ id: { $in: baseIds } }, baseIds.length, 0, true);
|
|
137
|
-
// keep only pure base users and sensible visibility
|
|
138
|
-
const validUsers = baseUsers.filter((u) => !('baseId' in u) &&
|
|
139
|
-
(!u.type || u.type === 'user') &&
|
|
140
|
-
(!u.status || u.status === 'active'));
|
|
141
|
-
const userMap = new Map(validUsers.map((u) => [String(u.id ?? u._id), u]));
|
|
142
|
-
items = items.map((row) => {
|
|
143
|
-
const bid = String(row?.baseId?._id ?? row?.baseId ?? '');
|
|
144
|
-
const u = bid ? userMap.get(bid) : undefined;
|
|
145
|
-
const merged = u
|
|
146
|
-
? {
|
|
147
|
-
...row,
|
|
148
|
-
firstName: u.firstName ?? row.firstName,
|
|
149
|
-
lastName: u.lastName ?? row.lastName,
|
|
150
|
-
profilePicture: u.profilePicture ?? row.profilePicture,
|
|
151
|
-
status: u.status ?? row.status,
|
|
152
|
-
type: u.type ?? row.type,
|
|
153
|
-
email: u.email ?? row.email,
|
|
154
|
-
phone: u.phone ?? row.phone,
|
|
155
|
-
}
|
|
156
|
-
: row;
|
|
157
|
-
// remove baseId before returning
|
|
158
|
-
if ('baseId' in merged)
|
|
159
|
-
delete merged.baseId;
|
|
160
|
-
return merged;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
items = items.map((row) => {
|
|
165
|
-
if ('baseId' in row)
|
|
166
|
-
delete row.baseId;
|
|
167
|
-
return row;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
// Not extended — ensure baseId removed if present
|
|
173
|
-
items = items.map((row) => {
|
|
174
|
-
if ('baseId' in row)
|
|
175
|
-
delete row.baseId;
|
|
176
|
-
return row;
|
|
177
|
-
});
|
|
178
|
-
}
|
|
130
|
+
// Enrichment: If model extends Users, the adapter already hydrated basic fields.
|
|
131
|
+
// We just ensure baseId is removed for the response.
|
|
132
|
+
items = items.map((row) => {
|
|
133
|
+
if ('baseId' in row)
|
|
134
|
+
delete row.baseId;
|
|
135
|
+
return row;
|
|
136
|
+
});
|
|
179
137
|
results[modelNameLC] = Array.isArray(items)
|
|
180
138
|
? items.slice(0, perModelLimit)
|
|
181
139
|
: [];
|
|
@@ -157,54 +157,13 @@ function mountSearchRoutes(ctx) {
|
|
|
157
157
|
return it;
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
|
-
// If model extends Users,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const baseUsers = await userModel.read({ id: { $in: baseIds } }, baseIds.length, 0, true);
|
|
168
|
-
const validUsers = baseUsers.filter((u) => !('baseId' in u) &&
|
|
169
|
-
(!u.type || u.type === 'user') &&
|
|
170
|
-
(!u.status || u.status === 'active'));
|
|
171
|
-
const userMap = new Map(validUsers.map((u) => [String(u.id ?? u._id), u]));
|
|
172
|
-
items = items.map((row) => {
|
|
173
|
-
const bid = String(row?.baseId?._id ?? row?.baseId ?? '');
|
|
174
|
-
const u = bid ? userMap.get(bid) : undefined;
|
|
175
|
-
const merged = u
|
|
176
|
-
? {
|
|
177
|
-
...row,
|
|
178
|
-
firstName: u.firstName ?? row.firstName,
|
|
179
|
-
lastName: u.lastName ?? row.lastName,
|
|
180
|
-
profilePicture: u.profilePicture ?? row.profilePicture,
|
|
181
|
-
status: u.status ?? row.status,
|
|
182
|
-
type: u.type ?? row.type,
|
|
183
|
-
email: u.email ?? row.email,
|
|
184
|
-
phone: u.phone ?? row.phone,
|
|
185
|
-
}
|
|
186
|
-
: row;
|
|
187
|
-
if ('baseId' in merged)
|
|
188
|
-
delete merged.baseId;
|
|
189
|
-
return merged;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
items = items.map((row) => {
|
|
194
|
-
if ('baseId' in row)
|
|
195
|
-
delete row.baseId;
|
|
196
|
-
return row;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
// Not extended — ensure baseId removed if present
|
|
202
|
-
items = items.map((row) => {
|
|
203
|
-
if ('baseId' in row)
|
|
204
|
-
delete row.baseId;
|
|
205
|
-
return row;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
160
|
+
// Enrichment: If model extends Users, the adapter already hydrated basic fields.
|
|
161
|
+
// We just ensure baseId is removed for the response.
|
|
162
|
+
items = items.map((row) => {
|
|
163
|
+
if ('baseId' in row)
|
|
164
|
+
delete row.baseId;
|
|
165
|
+
return row;
|
|
166
|
+
});
|
|
208
167
|
results[modelNameLC] = Array.isArray(items)
|
|
209
168
|
? items.slice(0, perModelLimit)
|
|
210
169
|
: [];
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.setupFileRoutes = setupFileRoutes;
|
|
7
7
|
const multer_1 = __importDefault(require("multer"));
|
|
8
|
+
const Logger_1 = __importDefault(require("../../utils/Logger"));
|
|
8
9
|
function setupFileRoutes(ctx) {
|
|
9
10
|
if (!ctx.fileStorage)
|
|
10
11
|
return;
|
|
@@ -53,6 +54,46 @@ function setupFileRoutes(ctx) {
|
|
|
53
54
|
.json({ error: true, message: err?.message ?? 'Upload failed' });
|
|
54
55
|
}
|
|
55
56
|
});
|
|
57
|
+
ctx.router.get('/files/:key(*)', async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
let key = String(req.params.key || '');
|
|
60
|
+
if (!key) {
|
|
61
|
+
return res.status(400).json({ error: true, message: 'Key is required' });
|
|
62
|
+
}
|
|
63
|
+
// Ensure no leading slash for storage providers
|
|
64
|
+
key = key.replace(/^\/+/, '');
|
|
65
|
+
Logger_1.default.info('FileProxy', `Fetching file from storage: ${key}`);
|
|
66
|
+
const { body, contentType, size } = await ctx.fileStorage.download(key);
|
|
67
|
+
if (contentType)
|
|
68
|
+
res.setHeader('Content-Type', contentType);
|
|
69
|
+
if (size)
|
|
70
|
+
res.setHeader('Content-Length', size);
|
|
71
|
+
// Cache for 1 year if it's an image
|
|
72
|
+
if (contentType?.startsWith('image/')) {
|
|
73
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
77
|
+
}
|
|
78
|
+
if (body && typeof body.pipe === 'function') {
|
|
79
|
+
body.pipe(res);
|
|
80
|
+
}
|
|
81
|
+
else if (body && typeof body.transformToByteArray === 'function') {
|
|
82
|
+
const data = await body.transformToByteArray();
|
|
83
|
+
res.send(Buffer.from(data));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
res.send(body);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
Logger_1.default.warn('FileProxy', `Failed to fetch file ${req.params.key}: ${err.message}`);
|
|
91
|
+
if (err.name === 'NoSuchKey' || err.code === 'NoSuchKey' || err.name === 'NotFound' || err.code === 'NotFound') {
|
|
92
|
+
return res.status(404).json({ error: true, message: 'File not found in storage' });
|
|
93
|
+
}
|
|
94
|
+
return res.status(500).json({ error: true, message: err?.message ?? 'Download failed' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
56
97
|
ctx.router.post('/files', fileAuthMiddleware, upload.any(), async (req, res) => {
|
|
57
98
|
try {
|
|
58
99
|
const files = req.files ?? [];
|
|
@@ -55,6 +55,8 @@ export type ParsedQuery = {
|
|
|
55
55
|
skip: number;
|
|
56
56
|
projection?: Record<string, 1>;
|
|
57
57
|
sort: SortSpec;
|
|
58
|
+
sample?: number;
|
|
59
|
+
_populate?: Record<string, Record<string, 1>>;
|
|
58
60
|
};
|
|
59
61
|
export declare function parseQuery(req: Request, allowedKeys?: Set<string>): ParsedQuery;
|
|
60
62
|
export declare function extractIds(val: unknown): string[];
|
package/dist/api/router/utils.js
CHANGED
|
@@ -50,10 +50,15 @@ function toIdString(v) {
|
|
|
50
50
|
if (typeof v === 'object') {
|
|
51
51
|
if (typeof v.id === 'string')
|
|
52
52
|
return v.id;
|
|
53
|
+
if (typeof v.toString === 'function' && v.constructor.name === 'ObjectId')
|
|
54
|
+
return v.toString();
|
|
53
55
|
if (v._id && typeof v._id.toString === 'function')
|
|
54
56
|
return v._id.toString();
|
|
55
57
|
if (typeof v._id === 'string')
|
|
56
58
|
return v._id;
|
|
59
|
+
// Fallback for generic objects that might be IDs
|
|
60
|
+
if (typeof v.toString === 'function' && !Array.isArray(v))
|
|
61
|
+
return v.toString();
|
|
57
62
|
}
|
|
58
63
|
return null;
|
|
59
64
|
}
|
|
@@ -82,7 +87,9 @@ function splitFilterForExtended(filter, baseKeys) {
|
|
|
82
87
|
outBase[k] = baseArr;
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
85
|
-
if (
|
|
90
|
+
if (exports.VIRTUAL_SORT_KEYS.has(k))
|
|
91
|
+
outChild[k] = v;
|
|
92
|
+
else if (baseKeys.has(k))
|
|
86
93
|
outBase[k] = v;
|
|
87
94
|
else
|
|
88
95
|
outChild[k] = v;
|
|
@@ -95,10 +102,15 @@ function splitSortForExtended(sort, baseKeys) {
|
|
|
95
102
|
const child = {};
|
|
96
103
|
const base = {};
|
|
97
104
|
for (const [k, dir] of Object.entries(sort || {})) {
|
|
98
|
-
if (
|
|
105
|
+
if (exports.VIRTUAL_SORT_KEYS.has(k)) {
|
|
106
|
+
child[k] = dir;
|
|
107
|
+
}
|
|
108
|
+
else if (baseKeys.has(k)) {
|
|
99
109
|
base[k] = dir;
|
|
100
|
-
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
101
112
|
child[k] = dir;
|
|
113
|
+
}
|
|
102
114
|
}
|
|
103
115
|
return { child, base };
|
|
104
116
|
}
|
|
@@ -257,14 +269,15 @@ function parseSort(sortRaw, sortTypeRaw, allowedKeys) {
|
|
|
257
269
|
// client sent sort but none validated → honor “no default”
|
|
258
270
|
return spec;
|
|
259
271
|
}
|
|
260
|
-
// no sort sent → default
|
|
261
|
-
return { createdAt: -1 };
|
|
272
|
+
// no sort sent → default cascade
|
|
273
|
+
return { createdAt: -1, _id: -1, id: -1 };
|
|
262
274
|
}
|
|
263
275
|
function parseQuery(req, allowedKeys) {
|
|
264
|
-
const limit = Math.min(Number.parseInt(String(req.query.limit ?? '12'), 10) || 12,
|
|
276
|
+
const limit = Math.min(Number.parseInt(String(req.query.limit ?? '12'), 10) || 12, 100000);
|
|
265
277
|
const page = Math.max(Number.parseInt(String(req.query.page ?? '1'), 10) || 1, 1);
|
|
266
278
|
const skip = (page - 1) * limit;
|
|
267
|
-
const
|
|
279
|
+
const fieldsRaw = req.query.fields ?? req.query.select;
|
|
280
|
+
const fields = String(fieldsRaw ?? '')
|
|
268
281
|
.split(',')
|
|
269
282
|
.map((s) => s.trim())
|
|
270
283
|
.filter(Boolean);
|
|
@@ -274,7 +287,23 @@ function parseQuery(req, allowedKeys) {
|
|
|
274
287
|
// NOTE: do NOT force a fallback like '-createdAt' here;
|
|
275
288
|
// parseSort will apply { createdAt: -1 } only if no sort is provided.
|
|
276
289
|
const sort = parseSort(req.query.sort, req.query.sortType, allowedKeys);
|
|
277
|
-
|
|
290
|
+
const sampleRaw = req.query.sample;
|
|
291
|
+
const sample = sampleRaw ? Math.max(Number.parseInt(String(sampleRaw), 10) || 0, 0) : undefined;
|
|
292
|
+
const populateMap = {};
|
|
293
|
+
const populateRaw = String(req.query.populate ?? '').trim();
|
|
294
|
+
if (populateRaw) {
|
|
295
|
+
const parts = populateRaw.split(';');
|
|
296
|
+
for (const part of parts) {
|
|
297
|
+
const [field, subFieldsRaw] = part.split(':');
|
|
298
|
+
if (field && subFieldsRaw) {
|
|
299
|
+
const subFields = subFieldsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
300
|
+
if (subFields.length > 0) {
|
|
301
|
+
populateMap[field] = Object.fromEntries(subFields.map(f => [f, 1]));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { limit, page, skip, projection, sort, sample, _populate: populateMap };
|
|
278
307
|
}
|
|
279
308
|
function extractIds(val) {
|
|
280
309
|
if (val == null)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const NMAdapter_1 = require("./database/NMAdapter");
|
|
7
|
+
const SchemaLoader_1 = require("./utils/SchemaLoader");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
10
|
+
dotenv_1.default.config();
|
|
11
|
+
async function repairMongo() {
|
|
12
|
+
const mongoUri = process.env.MONGODB_URI;
|
|
13
|
+
const dbName = process.env.DB_NAME || 'test';
|
|
14
|
+
const schemasDir = process.env.SCHEMAS_DIR || path_1.default.join(process.cwd(), 'schemas');
|
|
15
|
+
if (!mongoUri) {
|
|
16
|
+
console.error('MONGODB_URI is required in .env');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.log('--- NextMin MongoDB Repair Tool ---');
|
|
20
|
+
console.log(`Connecting to: ${mongoUri}`);
|
|
21
|
+
const loader = SchemaLoader_1.SchemaLoader.getInstance();
|
|
22
|
+
const schemaDefs = loader.getSchemas();
|
|
23
|
+
const adapter = new NMAdapter_1.NMAdapter({
|
|
24
|
+
type: 'mongodb',
|
|
25
|
+
url: mongoUri,
|
|
26
|
+
database: dbName,
|
|
27
|
+
useNewUrlParser: true,
|
|
28
|
+
useUnifiedTopology: true,
|
|
29
|
+
schemas: schemaDefs
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
await adapter.connect();
|
|
33
|
+
await adapter.registerSchemas(schemaDefs);
|
|
34
|
+
for (const [name, schema] of Object.entries(schemaDefs)) {
|
|
35
|
+
if (schema.extends) {
|
|
36
|
+
console.log(`Checking ${name} (extends ${schema.extends})...`);
|
|
37
|
+
const items = await adapter.read(name, {});
|
|
38
|
+
const broken = items.filter(it => !it.baseId);
|
|
39
|
+
if (broken.length > 0) {
|
|
40
|
+
console.log(`Found ${broken.length} items missing baseId in ${name}. Attempting repair...`);
|
|
41
|
+
for (const item of broken) {
|
|
42
|
+
const searchVal = item.slug || item.username || item.name;
|
|
43
|
+
if (!searchVal)
|
|
44
|
+
continue;
|
|
45
|
+
// Try to find matching base record
|
|
46
|
+
const baseMatches = await adapter.read(schema.extends, {
|
|
47
|
+
$or: [
|
|
48
|
+
{ slug: searchVal },
|
|
49
|
+
{ username: searchVal },
|
|
50
|
+
{ name: searchVal }
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
if (baseMatches.length > 0) {
|
|
54
|
+
const baseId = baseMatches[0].id;
|
|
55
|
+
await adapter.update(name, item.id, { baseId });
|
|
56
|
+
console.log(` ✅ Repaired ${name} item '${searchVal}' -> baseId: ${baseId}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn(` ❌ Could not find matching ${schema.extends} for ${name} item '${searchVal}'`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(` No broken links found in ${name}.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log('Repair process completed.');
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error('Repair failed:', err);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await adapter.disconnect();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const command = process.argv[2];
|
|
78
|
+
if (command === 'repair-mongo') {
|
|
79
|
+
repairMongo();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log('Usage: ts-node src/cli.ts repair-mongo');
|
|
83
|
+
}
|
|
@@ -10,5 +10,13 @@ export interface DatabaseAdapter {
|
|
|
10
10
|
update(collection: string, id: string, data: any, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<any>;
|
|
11
11
|
delete(collection: string, id: string, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<any>;
|
|
12
12
|
count(collection: string, query: any, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* Bulk existence check for related records.
|
|
15
|
+
* Returns a map ofparentId -> firstRelatedId
|
|
16
|
+
*/
|
|
17
|
+
findFirstRelatedIds(collection: string, field: string, ids: string[]): Promise<Map<string, string>>;
|
|
18
|
+
countBy(collection: string, field: string, query: any, schema: Schema): Promise<Record<string, number>>;
|
|
13
19
|
syncIndexes?(modelName: string, desired: FieldIndexSpec): Promise<void>;
|
|
20
|
+
cleanupUnusedFields?(schemas: Record<string, Schema>): Promise<Record<string, string[]>>;
|
|
21
|
+
isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: Schema | undefined): boolean;
|
|
14
22
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DatabaseAdapter, FieldIndexSpec } from './DatabaseAdapter';
|
|
2
|
+
import { ReadOptions, Schema as NextMinSchema } from '../models/BaseModel';
|
|
3
|
+
/**
|
|
4
|
+
* NMAdapter implements the DatabaseAdapter interface.
|
|
5
|
+
* It supports SQL databases (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) and MongoDB.
|
|
6
|
+
*/
|
|
7
|
+
export declare class NMAdapter implements DatabaseAdapter {
|
|
8
|
+
private options;
|
|
9
|
+
private dataSource;
|
|
10
|
+
private repositories;
|
|
11
|
+
private entitySchemas;
|
|
12
|
+
private registeredSchemas;
|
|
13
|
+
private imageProxyOptions;
|
|
14
|
+
constructor(options: any);
|
|
15
|
+
connect(): Promise<void>;
|
|
16
|
+
disconnect(): Promise<void>;
|
|
17
|
+
registerSchemas(schemas: Record<string, NextMinSchema>): Promise<void>;
|
|
18
|
+
syncIndexes(modelName: string, spec: FieldIndexSpec): Promise<void>;
|
|
19
|
+
private buildEntitySchema;
|
|
20
|
+
private mapType;
|
|
21
|
+
private getRepository;
|
|
22
|
+
create(collection: string, data: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
23
|
+
private coercePayloadForStorage;
|
|
24
|
+
read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: NextMinSchema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
|
|
25
|
+
private prepareResult;
|
|
26
|
+
private transformUrls;
|
|
27
|
+
private idToString;
|
|
28
|
+
private convertToObjectId;
|
|
29
|
+
private discoverInverseRelation;
|
|
30
|
+
private hydrateDynamicAggregates;
|
|
31
|
+
private hydrateRelations;
|
|
32
|
+
update(collection: string, targetId: any, data: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
33
|
+
delete(collection: string, targetId: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
34
|
+
count(collection: string, query: any, schemaDefinition?: NextMinSchema): Promise<number>;
|
|
35
|
+
countBy(collection: string, field: string, query: any, schema?: NextMinSchema): Promise<Record<string, number>>;
|
|
36
|
+
findFirstRelatedIds(collection: string, field: string, ids: string[]): Promise<Map<string, string>>;
|
|
37
|
+
/**
|
|
38
|
+
* Centralized Nested Search Resolver
|
|
39
|
+
* Recursively scans for $nestedSearch operators and resolves them to { $in: [ids] }
|
|
40
|
+
*/
|
|
41
|
+
private resolveNestedSearches;
|
|
42
|
+
private transformQuery;
|
|
43
|
+
private mapOperator;
|
|
44
|
+
cleanupUnusedFields(schemas: Record<string, NextMinSchema>): Promise<Record<string, string[]>>;
|
|
45
|
+
isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: NextMinSchema | undefined): boolean;
|
|
46
|
+
}
|