@airoom/nextmin-node 1.4.5 → 2.0.1
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 +2 -0
- package/dist/api/apiRouter.js +68 -19
- package/dist/api/router/mountCrudRoutes.js +209 -221
- 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/setupAuthRoutes.js +6 -2
- package/dist/api/router/utils.js +20 -7
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +7 -0
- package/dist/database/NMAdapter.d.ts +41 -0
- package/dist/database/NMAdapter.js +979 -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/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +16 -0
- package/dist/models/BaseModel.js +32 -4
- package/dist/policy/authorize.js +118 -43
- package/dist/schemas/Users.json +66 -30
- 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 +9 -5
- 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.js +12 -10
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +7 -2
- package/dist/utils/SchemaLoader.js +58 -18
- 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
|
@@ -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
|
: [];
|
|
@@ -114,7 +114,7 @@ function setupAuthRoutes(ctx) {
|
|
|
114
114
|
res.json({
|
|
115
115
|
success: true,
|
|
116
116
|
message: 'You are successfully logged in.',
|
|
117
|
-
data: { token, user },
|
|
117
|
+
data: { token, user: { ...user, roleName } },
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
catch (error) {
|
|
@@ -140,8 +140,12 @@ function setupAuthRoutes(ctx) {
|
|
|
140
140
|
.status(404)
|
|
141
141
|
.json({ error: true, message: 'User not found' });
|
|
142
142
|
}
|
|
143
|
+
const roleName = (await ctx.normalizeRoleName(user.role)) ?? '';
|
|
143
144
|
delete user.password;
|
|
144
|
-
return res.json({
|
|
145
|
+
return res.json({
|
|
146
|
+
success: true,
|
|
147
|
+
data: { ...user, roleName },
|
|
148
|
+
});
|
|
145
149
|
}
|
|
146
150
|
catch (error) {
|
|
147
151
|
return res
|
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);
|
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,12 @@ 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>>;
|
|
13
18
|
syncIndexes?(modelName: string, desired: FieldIndexSpec): Promise<void>;
|
|
19
|
+
cleanupUnusedFields?(schemas: Record<string, Schema>): Promise<Record<string, string[]>>;
|
|
20
|
+
isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: Schema | undefined): boolean;
|
|
14
21
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
constructor(options: any);
|
|
14
|
+
connect(): Promise<void>;
|
|
15
|
+
disconnect(): Promise<void>;
|
|
16
|
+
registerSchemas(schemas: Record<string, NextMinSchema>): Promise<void>;
|
|
17
|
+
syncIndexes(modelName: string, spec: FieldIndexSpec): Promise<void>;
|
|
18
|
+
private buildEntitySchema;
|
|
19
|
+
private mapType;
|
|
20
|
+
private getRepository;
|
|
21
|
+
create(collection: string, data: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
22
|
+
private coercePayloadForStorage;
|
|
23
|
+
read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: NextMinSchema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
|
|
24
|
+
private prepareResult;
|
|
25
|
+
private idToString;
|
|
26
|
+
private convertToObjectId;
|
|
27
|
+
private hydrateRelations;
|
|
28
|
+
update(collection: string, targetId: any, data: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
29
|
+
delete(collection: string, targetId: any, schemaDefinition?: NextMinSchema): Promise<any>;
|
|
30
|
+
count(collection: string, query: any, schemaDefinition?: NextMinSchema): Promise<number>;
|
|
31
|
+
findFirstRelatedIds(collection: string, field: string, ids: string[]): Promise<Map<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* Centralized Nested Search Resolver
|
|
34
|
+
* Recursively scans for $nestedSearch operators and resolves them to { $in: [ids] }
|
|
35
|
+
*/
|
|
36
|
+
private resolveNestedSearches;
|
|
37
|
+
private transformQuery;
|
|
38
|
+
private mapOperator;
|
|
39
|
+
cleanupUnusedFields(schemas: Record<string, NextMinSchema>): Promise<Record<string, string[]>>;
|
|
40
|
+
isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: NextMinSchema | undefined): boolean;
|
|
41
|
+
}
|