@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
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
exports.Events = exports.events = void 0;
|
|
7
|
+
exports.getModelEvent = getModelEvent;
|
|
8
|
+
const events_1 = require("events");
|
|
9
|
+
const Logger_1 = __importDefault(require("./Logger"));
|
|
10
|
+
/**
|
|
11
|
+
* NextMinEvents is a central event emitter for the NextMin ecosystem.
|
|
12
|
+
* It allows components to hook into CRUD and system events.
|
|
13
|
+
*/
|
|
14
|
+
class NextMinEvents extends events_1.EventEmitter {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
// Increase max listeners to avoid warnings in complex apps
|
|
18
|
+
this.setMaxListeners(100);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Typed emit to provide better DX (Developer Experience)
|
|
22
|
+
*/
|
|
23
|
+
emitEvent(event, payload) {
|
|
24
|
+
Logger_1.default.info('NextMinEvents', `Emitting event: ${event}`);
|
|
25
|
+
this.emit(event, payload);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Singleton instance
|
|
29
|
+
exports.events = new NextMinEvents();
|
|
30
|
+
// Standard event constants
|
|
31
|
+
exports.Events = {
|
|
32
|
+
// Document events
|
|
33
|
+
BEFORE_CREATE: 'before:doc:create',
|
|
34
|
+
AFTER_CREATE: 'after:doc:create',
|
|
35
|
+
BEFORE_UPDATE: 'before:doc:update',
|
|
36
|
+
AFTER_UPDATE: 'after:doc:update',
|
|
37
|
+
BEFORE_DELETE: 'before:doc:delete',
|
|
38
|
+
AFTER_DELETE: 'after:doc:delete',
|
|
39
|
+
BEFORE_READ: 'before:doc:read',
|
|
40
|
+
AFTER_READ: 'after:doc:read',
|
|
41
|
+
// Auth events
|
|
42
|
+
AUTH_LOGIN: 'auth:login',
|
|
43
|
+
AUTH_LOGOUT: 'auth:logout',
|
|
44
|
+
AUTH_SIGNUP: 'auth:signup',
|
|
45
|
+
// System events
|
|
46
|
+
SCHEMA_UPDATE: 'schema:update',
|
|
47
|
+
SERVER_START: 'server:start',
|
|
48
|
+
SERVER_STOP: 'server:stop',
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Helper to generate model-specific event names
|
|
52
|
+
*/
|
|
53
|
+
function getModelEvent(modelName, action, phase = 'after') {
|
|
54
|
+
return `${modelName.toLowerCase()}:${phase}:${action}`;
|
|
55
|
+
}
|
package/dist/utils/Logger.js
CHANGED
|
@@ -9,8 +9,7 @@ const util_1 = require("util");
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
class Logger {
|
|
11
11
|
constructor() {
|
|
12
|
-
|
|
13
|
-
this.isDevelopment = env === 'development';
|
|
12
|
+
this.isDevelopment = process.env.APP_MODE !== 'production' && process.env.NODE_ENV !== 'production';
|
|
14
13
|
}
|
|
15
14
|
getCallerInfo() {
|
|
16
15
|
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
@@ -24,12 +23,11 @@ class Logger {
|
|
|
24
23
|
!frame.getFileName()?.includes('Logger.ts') &&
|
|
25
24
|
!frame.getFunctionName()?.includes('getCallerInfo'));
|
|
26
25
|
if (!callerFrame) {
|
|
27
|
-
return 'unknown:0
|
|
26
|
+
return 'unknown:0';
|
|
28
27
|
}
|
|
29
|
-
const fileName = path_1.default.
|
|
28
|
+
const fileName = path_1.default.basename(callerFrame.getFileName() || 'unknown');
|
|
30
29
|
const lineNumber = callerFrame.getLineNumber() || 0;
|
|
31
|
-
|
|
32
|
-
return `${fileName}:${lineNumber}:${columnNumber}`;
|
|
30
|
+
return `${fileName}:${lineNumber}`;
|
|
33
31
|
}
|
|
34
32
|
formatMessage(level, label, messages) {
|
|
35
33
|
const callerInfo = this.getCallerInfo();
|
|
@@ -66,12 +64,16 @@ class Logger {
|
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
warn(label, ...messages) {
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
if (this.isDevelopment) {
|
|
68
|
+
const formatted = this.formatMessage('WARN', label, messages);
|
|
69
|
+
console.warn(formatted);
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
error(label, ...messages) {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if (this.isDevelopment) {
|
|
74
|
+
const formatted = this.formatMessage('ERROR', label, messages);
|
|
75
|
+
console.error(formatted);
|
|
76
|
+
}
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
exports.Logger = Logger;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CacheConfig {
|
|
2
|
+
ttl?: number;
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare class QueryCache {
|
|
6
|
+
private cache;
|
|
7
|
+
private enabled;
|
|
8
|
+
private defaultTtl;
|
|
9
|
+
constructor(config?: CacheConfig);
|
|
10
|
+
generateKey(collection: string, operation: string, args: any): Promise<string>;
|
|
11
|
+
private sortObjectKeys;
|
|
12
|
+
get<T>(key: string): Promise<T | null>;
|
|
13
|
+
set(key: string, value: any, ttl?: number): Promise<void>;
|
|
14
|
+
invalidateCollection(collection: string): Promise<void>;
|
|
15
|
+
private getCollectionVersion;
|
|
16
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
exports.QueryCache = void 0;
|
|
7
|
+
const keyv_1 = __importDefault(require("keyv"));
|
|
8
|
+
const redis_1 = __importDefault(require("@keyv/redis"));
|
|
9
|
+
const Logger_1 = __importDefault(require("./Logger"));
|
|
10
|
+
class QueryCache {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.enabled = config.enabled !== false; // Enabled by default
|
|
13
|
+
this.defaultTtl = config.ttl || 60000; // 60 seconds default
|
|
14
|
+
const redisUrl = process.env.REDIS_URL;
|
|
15
|
+
if (redisUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const store = new redis_1.default(redisUrl);
|
|
18
|
+
this.cache = new keyv_1.default({ store });
|
|
19
|
+
Logger_1.default.info('QueryCache', `Initialized with Redis store at ${redisUrl}`);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
Logger_1.default.error('QueryCache', 'Failed to initialize Redis store, falling back to basic map', err);
|
|
23
|
+
this.cache = new keyv_1.default();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Falls back to in-memory map
|
|
28
|
+
this.cache = new keyv_1.default();
|
|
29
|
+
Logger_1.default.info('QueryCache', 'Initialized with in-memory store (no REDIS_URL provided)');
|
|
30
|
+
}
|
|
31
|
+
this.cache.on('error', (err) => Logger_1.default.warn('QueryCache', 'Connection Error', err));
|
|
32
|
+
}
|
|
33
|
+
async generateKey(collection, operation, args) {
|
|
34
|
+
try {
|
|
35
|
+
const version = await this.getCollectionVersion(collection);
|
|
36
|
+
const sortedArgs = this.sortObjectKeys(args);
|
|
37
|
+
const strArgs = JSON.stringify(sortedArgs);
|
|
38
|
+
return `nm:cache:${collection}:v${version}:${operation}:${strArgs}`;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Fallback if JSON.stringify fails (e.g. circular refs, though unlikely in simple queries)
|
|
42
|
+
const v = await this.getCollectionVersion(collection);
|
|
43
|
+
return `nm:cache:${collection}:v${v}:${operation}:${Date.now()}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
sortObjectKeys(obj) {
|
|
47
|
+
if (obj === null || typeof obj !== 'object') {
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(obj)) {
|
|
51
|
+
return obj.map(item => this.sortObjectKeys(item));
|
|
52
|
+
}
|
|
53
|
+
const sortedObj = {};
|
|
54
|
+
Object.keys(obj).sort().forEach(key => {
|
|
55
|
+
sortedObj[key] = this.sortObjectKeys(obj[key]);
|
|
56
|
+
});
|
|
57
|
+
return sortedObj;
|
|
58
|
+
}
|
|
59
|
+
async get(key) {
|
|
60
|
+
if (!this.enabled)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
const data = await this.cache.get(key);
|
|
64
|
+
if (data) {
|
|
65
|
+
// Return a deep clone to prevent external mutation of cached objects
|
|
66
|
+
return JSON.parse(JSON.stringify(data));
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
Logger_1.default.warn('QueryCache', `Failed to get key ${key}`, err);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async set(key, value, ttl) {
|
|
76
|
+
if (!this.enabled || value === undefined)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
// Store a clean copy
|
|
80
|
+
const cleanValue = JSON.parse(JSON.stringify(value));
|
|
81
|
+
await this.cache.set(key, cleanValue, ttl || this.defaultTtl);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
Logger_1.default.warn('QueryCache', `Failed to set key ${key}`, err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async invalidateCollection(collection) {
|
|
88
|
+
if (!this.enabled)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
// Increment collection version:
|
|
92
|
+
const vKey = `nm:collection:version:${collection}`;
|
|
93
|
+
let currentVersion = await this.cache.get(vKey) || 0;
|
|
94
|
+
await this.cache.set(vKey, currentVersion + 1, 0); // never expire the version
|
|
95
|
+
Logger_1.default.info('QueryCache', `Invalidated cache for collection: ${collection} (version bumped)`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
Logger_1.default.warn('QueryCache', `Failed to invalidate collection ${collection}`, err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getCollectionVersion(collection) {
|
|
102
|
+
const vKey = `nm:collection:version:${collection}`;
|
|
103
|
+
return (await this.cache.get(vKey)) || 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.QueryCache = QueryCache;
|
|
@@ -6,6 +6,11 @@ type PublicSchema = Omit<Schema, 'attributes'> & {
|
|
|
6
6
|
showCount: boolean;
|
|
7
7
|
attributes: PublicAttributes;
|
|
8
8
|
allowedMethods: Schema['allowedMethods'];
|
|
9
|
+
extends?: string;
|
|
10
|
+
group?: string;
|
|
11
|
+
schemaType?: string;
|
|
12
|
+
columnsSelector?: string[];
|
|
13
|
+
actions?: Schema['actions'];
|
|
9
14
|
};
|
|
10
15
|
export declare class SchemaLoader {
|
|
11
16
|
private emitter;
|
|
@@ -33,9 +38,9 @@ export declare class SchemaLoader {
|
|
|
33
38
|
[key: string]: Schema;
|
|
34
39
|
};
|
|
35
40
|
/** CLIENT/API: sanitized map with private attributes removed */
|
|
36
|
-
getPublicSchemas(): Record<string, PublicSchema>;
|
|
41
|
+
getPublicSchemas(showPrivate?: boolean): Record<string, PublicSchema>;
|
|
37
42
|
/** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
|
|
38
|
-
getPublicSchemaList(): Array<PublicSchema>;
|
|
43
|
+
getPublicSchemaList(showPrivate?: boolean): Array<PublicSchema>;
|
|
39
44
|
/** Strip any attr marked private; also remove the `private` flag from others */
|
|
40
45
|
/** Keep private/sensitive flags so the UI and policy layer can decide.
|
|
41
46
|
* We only shallow-clone values to avoid leaking references.
|
|
@@ -138,13 +138,14 @@ class SchemaLoader {
|
|
|
138
138
|
continue;
|
|
139
139
|
if (Object.prototype.hasOwnProperty.call(mergedAttrs, key)) {
|
|
140
140
|
const val = mergedAttrs[key];
|
|
141
|
+
const isInherited = !Object.prototype.hasOwnProperty.call(schema.attributes, key);
|
|
141
142
|
if (Array.isArray(val) && val[0] && typeof val[0] === 'object') {
|
|
142
143
|
const { unique: _u, index: _i, ...rest } = val[0];
|
|
143
|
-
mergedAttrs[key] = [{ ...rest }];
|
|
144
|
+
mergedAttrs[key] = [{ ...rest, inherited: isInherited }];
|
|
144
145
|
}
|
|
145
146
|
else if (val && typeof val === 'object') {
|
|
146
147
|
const { unique: _u, index: _i, ...rest } = val;
|
|
147
|
-
mergedAttrs[key] = { ...rest };
|
|
148
|
+
mergedAttrs[key] = { ...rest, inherited: isInherited };
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
}
|
|
@@ -153,14 +154,50 @@ class SchemaLoader {
|
|
|
153
154
|
...baseSchema.allowedMethods,
|
|
154
155
|
...schema.allowedMethods,
|
|
155
156
|
};
|
|
157
|
+
// Merge access (policies/masks/reachability)
|
|
158
|
+
if (baseSchema.access) {
|
|
159
|
+
const baseAccess = baseSchema.access;
|
|
160
|
+
if (!schema.access)
|
|
161
|
+
schema.access = {};
|
|
162
|
+
// 1. Selective Merge for bypassPrivacy (combine roles)
|
|
163
|
+
if (baseAccess.bypassPrivacy?.roles) {
|
|
164
|
+
if (!schema.access.bypassPrivacy)
|
|
165
|
+
schema.access.bypassPrivacy = {};
|
|
166
|
+
const baseRoles = baseAccess.bypassPrivacy.roles;
|
|
167
|
+
const currentRoles = schema.access.bypassPrivacy.roles || [];
|
|
168
|
+
schema.access.bypassPrivacy.roles = Array.from(new Set([...baseRoles, ...currentRoles]));
|
|
169
|
+
}
|
|
170
|
+
// 2. Inherit masks/filters/restrictions ONLY if not defined in child
|
|
171
|
+
// Note: We DO NOT inherit public/authenticated/roles reachability here.
|
|
172
|
+
// Child schemas should manage their own reachability.
|
|
173
|
+
if (!schema.access.readMask)
|
|
174
|
+
schema.access.readMask = baseAccess.readMask;
|
|
175
|
+
if (!schema.access.writeDeny)
|
|
176
|
+
schema.access.writeDeny = baseAccess.writeDeny;
|
|
177
|
+
if (!schema.access.queryFilter)
|
|
178
|
+
schema.access.queryFilter = baseAccess.queryFilter;
|
|
179
|
+
if (!schema.access.createDefaults)
|
|
180
|
+
schema.access.createDefaults = baseAccess.createDefaults;
|
|
181
|
+
if (!schema.access.restrictions)
|
|
182
|
+
schema.access.restrictions = baseAccess.restrictions;
|
|
183
|
+
if (!schema.access.conditions)
|
|
184
|
+
schema.access.conditions = baseAccess.conditions;
|
|
185
|
+
}
|
|
156
186
|
// Inject hidden link field to base (used for storage join)
|
|
157
187
|
// Ensure hidden link field
|
|
158
188
|
const linkField = 'baseId';
|
|
159
189
|
if (!schema.attributes[linkField]) {
|
|
190
|
+
const isUserExt = baseName.toLowerCase() === 'users';
|
|
160
191
|
schema.attributes[linkField] = {
|
|
161
192
|
type: 'ObjectId',
|
|
162
193
|
ref: baseName,
|
|
163
|
-
private:
|
|
194
|
+
private: !isUserExt,
|
|
195
|
+
required: false,
|
|
196
|
+
label: isUserExt ? 'Associate User Account (Optional)' : 'Base Record',
|
|
197
|
+
show: isUserExt ? 'username' : 'id',
|
|
198
|
+
index: true,
|
|
199
|
+
unique: isUserExt,
|
|
200
|
+
sparse: isUserExt,
|
|
164
201
|
};
|
|
165
202
|
}
|
|
166
203
|
}
|
|
@@ -288,13 +325,13 @@ class SchemaLoader {
|
|
|
288
325
|
return this.schemas;
|
|
289
326
|
}
|
|
290
327
|
/** CLIENT/API: sanitized map with private attributes removed */
|
|
291
|
-
getPublicSchemas() {
|
|
328
|
+
getPublicSchemas(showPrivate = false) {
|
|
292
329
|
const out = {};
|
|
293
330
|
for (const [name, s] of Object.entries(this.schemas)) {
|
|
294
331
|
// if (this.nonOverridableSchemas.has(name)) continue;
|
|
295
332
|
// Clone sanitized attributes and add timestamps
|
|
296
333
|
const attributesWithTimestamps = {
|
|
297
|
-
...this.sanitizeAttributes(s.attributes),
|
|
334
|
+
...this.sanitizeAttributes(s.attributes, showPrivate),
|
|
298
335
|
createdAt: { type: 'datetime' },
|
|
299
336
|
updatedAt: { type: 'datetime' },
|
|
300
337
|
};
|
|
@@ -303,20 +340,25 @@ class SchemaLoader {
|
|
|
303
340
|
showCount: s.showCount,
|
|
304
341
|
allowedMethods: s.allowedMethods,
|
|
305
342
|
attributes: attributesWithTimestamps,
|
|
343
|
+
extends: s.extends,
|
|
344
|
+
group: s.group,
|
|
345
|
+
schemaType: s.schemaType,
|
|
346
|
+
columnsSelector: s.columnsSelector,
|
|
347
|
+
actions: s.actions,
|
|
306
348
|
};
|
|
307
349
|
}
|
|
308
350
|
return out;
|
|
309
351
|
}
|
|
310
352
|
/** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
|
|
311
|
-
getPublicSchemaList() {
|
|
312
|
-
const pub = this.getPublicSchemas();
|
|
353
|
+
getPublicSchemaList(showPrivate = false) {
|
|
354
|
+
const pub = this.getPublicSchemas(showPrivate);
|
|
313
355
|
return Object.values(pub);
|
|
314
356
|
}
|
|
315
357
|
/** Strip any attr marked private; also remove the `private` flag from others */
|
|
316
358
|
/** Keep private/sensitive flags so the UI and policy layer can decide.
|
|
317
359
|
* We only shallow-clone values to avoid leaking references.
|
|
318
360
|
*/
|
|
319
|
-
sanitizeAttributes(attrs) {
|
|
361
|
+
sanitizeAttributes(attrs, showPrivate = false) {
|
|
320
362
|
const out = {};
|
|
321
363
|
if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs))
|
|
322
364
|
return out;
|
|
@@ -325,13 +367,12 @@ class SchemaLoader {
|
|
|
325
367
|
if (Array.isArray(attr)) {
|
|
326
368
|
const elem = attr[0];
|
|
327
369
|
if (elem && typeof elem === 'object') {
|
|
328
|
-
// If the inner descriptor is private, omit this field entirely
|
|
329
|
-
if (elem.private) {
|
|
370
|
+
// If the inner descriptor is private and we're not showing private, omit this field entirely
|
|
371
|
+
if (elem.private && !showPrivate) {
|
|
330
372
|
continue;
|
|
331
373
|
}
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
out[key] = [{ ...rest }];
|
|
374
|
+
// Preserve the descriptor, including the 'private' flag if showPrivate is true
|
|
375
|
+
out[key] = [{ ...elem }];
|
|
335
376
|
}
|
|
336
377
|
else {
|
|
337
378
|
// Fallback: keep as-is (no private flag to check)
|
|
@@ -341,13 +382,12 @@ class SchemaLoader {
|
|
|
341
382
|
}
|
|
342
383
|
// Single attribute object
|
|
343
384
|
if (attr && typeof attr === 'object') {
|
|
344
|
-
// If marked private, omit from public schema entirely
|
|
345
|
-
if (attr.private) {
|
|
385
|
+
// If marked private and we're not showing private, omit from public schema entirely
|
|
386
|
+
if (attr.private && !showPrivate) {
|
|
346
387
|
continue;
|
|
347
388
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
out[key] = { ...rest };
|
|
389
|
+
// Preserve the descriptor, including the 'private' flag if showPrivate is true
|
|
390
|
+
out[key] = { ...attr };
|
|
351
391
|
continue;
|
|
352
392
|
}
|
|
353
393
|
// Unexpected primitives — pass through
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airoom/nextmin-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "rm -rf dist && tsc --project tsconfig.json && copyfiles -u 2 \"src/schemas/**/*\" dist/schemas",
|
|
9
9
|
"watch": "tsc --project tsconfig.json --watch",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"test:coverage": "vitest run --coverage",
|
|
10
13
|
"prepublishOnly": "npm run build && npm pack --dry-run | (! grep -E '\\bsrc/|\\.map$')"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
@@ -17,19 +20,31 @@
|
|
|
17
20
|
"chokidar": "^4.0.3",
|
|
18
21
|
"fast-glob": "^3.3.1",
|
|
19
22
|
"jsonwebtoken": "^9.0.2",
|
|
23
|
+
"keyv": "^5.2.3",
|
|
20
24
|
"kleur": "^4.1.5",
|
|
21
|
-
"
|
|
25
|
+
"@keyv/redis": "^4.0.1",
|
|
26
|
+
"mongodb": "^6.21.0",
|
|
27
|
+
"mongoose": "^8.18.0",
|
|
22
28
|
"mongoose-autopopulate": "^1.1.0",
|
|
29
|
+
"mssql": "^12.2.0",
|
|
23
30
|
"multer": "^2.0.2",
|
|
24
|
-
"
|
|
31
|
+
"mysql2": "^3.16.1",
|
|
32
|
+
"pg": "^8.17.2",
|
|
33
|
+
"reflect-metadata": "^0.2.2",
|
|
34
|
+
"socket.io": "^4.7.5",
|
|
35
|
+
"sqlite3": "^5.1.7",
|
|
36
|
+
"typeorm": "^0.3.28"
|
|
25
37
|
},
|
|
26
38
|
"devDependencies": {
|
|
27
39
|
"@types/bcrypt": "^6.0.0",
|
|
28
40
|
"@types/chokidar": "^2.1.7",
|
|
29
41
|
"@types/jsonwebtoken": "^9.0.10",
|
|
30
42
|
"@types/multer": "^2.0.0",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
31
44
|
"copyfiles": "^2.4.1",
|
|
32
|
-
"
|
|
45
|
+
"ts-node": "^10.9.2",
|
|
46
|
+
"typescript": "^5.3.3",
|
|
47
|
+
"vitest": "^4.0.17"
|
|
33
48
|
},
|
|
34
49
|
"files": [
|
|
35
50
|
"dist",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { DatabaseAdapter, FieldIndexSpec } from './DatabaseAdapter';
|
|
2
|
-
import { ReadOptions, Schema } from '../models/BaseModel';
|
|
3
|
-
export declare class InMemoryAdapter implements DatabaseAdapter {
|
|
4
|
-
private data;
|
|
5
|
-
connect(): Promise<void>;
|
|
6
|
-
disconnect(): Promise<void>;
|
|
7
|
-
registerSchemas(schemas: Record<string, Schema>): void;
|
|
8
|
-
create(collection: string, data: any, schemaDefinition: Schema): Promise<any>;
|
|
9
|
-
read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: Schema, _includePrivateFields?: boolean, _options?: ReadOptions): Promise<any[]>;
|
|
10
|
-
update(collection: string, id: string, data: any, schemaDefinition: Schema): Promise<any>;
|
|
11
|
-
delete(collection: string, id: string, schemaDefinition: Schema): Promise<any>;
|
|
12
|
-
count(collection: string, query: any, schemaDefinition: Schema): Promise<number>;
|
|
13
|
-
syncIndexes(_modelName: string, _desired: FieldIndexSpec): Promise<void>;
|
|
14
|
-
private generateId;
|
|
15
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InMemoryAdapter = void 0;
|
|
4
|
-
class InMemoryAdapter {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.data = {};
|
|
7
|
-
}
|
|
8
|
-
async connect() {
|
|
9
|
-
// No-op for in-memory
|
|
10
|
-
}
|
|
11
|
-
async disconnect() {
|
|
12
|
-
// No-op for in-memory
|
|
13
|
-
}
|
|
14
|
-
registerSchemas(schemas) {
|
|
15
|
-
// No-op for in-memory, but you could validate schemas here if needed
|
|
16
|
-
}
|
|
17
|
-
async create(collection, data, schemaDefinition) {
|
|
18
|
-
if (!this.data[collection]) {
|
|
19
|
-
this.data[collection] = [];
|
|
20
|
-
}
|
|
21
|
-
// Generate a simple id if not present
|
|
22
|
-
if (!data.id) {
|
|
23
|
-
data.id = `${Date.now()}_${Math.random()}`;
|
|
24
|
-
}
|
|
25
|
-
this.data[collection].push(data);
|
|
26
|
-
return data;
|
|
27
|
-
}
|
|
28
|
-
async read(collection, query, limit, skip, schemaDefinition, _includePrivateFields, _options) {
|
|
29
|
-
const allData = this.data[collection] || [];
|
|
30
|
-
let results = allData.filter((item) => {
|
|
31
|
-
return Object.entries(query).every(([key, value]) => item[key] === value);
|
|
32
|
-
});
|
|
33
|
-
if (limit !== undefined && skip !== undefined) {
|
|
34
|
-
results = results.slice(skip, skip + limit);
|
|
35
|
-
}
|
|
36
|
-
return results;
|
|
37
|
-
}
|
|
38
|
-
async update(collection, id, data, schemaDefinition) {
|
|
39
|
-
const allData = this.data[collection] || [];
|
|
40
|
-
const index = allData.findIndex((item) => item.id === id);
|
|
41
|
-
if (index === -1) {
|
|
42
|
-
throw new Error(`Document with id ${id} not found in collection '${collection}'`);
|
|
43
|
-
}
|
|
44
|
-
this.data[collection][index] = { ...allData[index], ...data };
|
|
45
|
-
return this.data[collection][index];
|
|
46
|
-
}
|
|
47
|
-
async delete(collection, id, schemaDefinition) {
|
|
48
|
-
const allData = this.data[collection] || [];
|
|
49
|
-
const index = allData.findIndex((item) => item.id === id);
|
|
50
|
-
if (index === -1) {
|
|
51
|
-
throw new Error(`Document with id ${id} not found in collection '${collection}'`);
|
|
52
|
-
}
|
|
53
|
-
const deleted = this.data[collection].splice(index, 1)[0];
|
|
54
|
-
return deleted;
|
|
55
|
-
}
|
|
56
|
-
async count(collection, query, schemaDefinition) {
|
|
57
|
-
const allData = this.data[collection] || [];
|
|
58
|
-
// Count documents matching the query
|
|
59
|
-
const results = allData.filter((item) => {
|
|
60
|
-
return Object.entries(query).every(([key, value]) => item[key] === value);
|
|
61
|
-
});
|
|
62
|
-
return results.length;
|
|
63
|
-
}
|
|
64
|
-
async syncIndexes(_modelName, _desired) {
|
|
65
|
-
// no-op for memory adapter
|
|
66
|
-
}
|
|
67
|
-
generateId() {
|
|
68
|
-
return Math.random().toString(36).substr(2, 9);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
exports.InMemoryAdapter = InMemoryAdapter;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { DatabaseAdapter, FieldIndexSpec } from './DatabaseAdapter';
|
|
2
|
-
import { ReadOptions, Schema as NextMinSchema } from '../models/BaseModel';
|
|
3
|
-
export declare class MongoAdapter implements DatabaseAdapter {
|
|
4
|
-
private url;
|
|
5
|
-
private dbName;
|
|
6
|
-
private connection;
|
|
7
|
-
/** Map keyed by lowercased modelName (e.g., "users", "roles") → Mongoose Model */
|
|
8
|
-
private models;
|
|
9
|
-
constructor(url: string, dbName: string);
|
|
10
|
-
connect(): Promise<void>;
|
|
11
|
-
disconnect(): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
-
* Register (and re-register) all schemas.
|
|
14
|
-
* - Drops compiled models that no longer exist.
|
|
15
|
-
* - Recompiles every current model so validators reflect latest schema.
|
|
16
|
-
*/
|
|
17
|
-
registerSchemas(schemas: Record<string, NextMinSchema>): Promise<void>;
|
|
18
|
-
/** Optional hook used by the router when schemas are removed. */
|
|
19
|
-
unregisterSchemas(names: string[]): Promise<void>;
|
|
20
|
-
/** Optional single-model drop hook. */
|
|
21
|
-
dropModel(nameRaw: string): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Delete a compiled model by case-insensitive match.
|
|
24
|
-
* Works whether we compiled it as "Users" or "users".
|
|
25
|
-
*/
|
|
26
|
-
private safeDeleteModel;
|
|
27
|
-
private safeDropCollection;
|
|
28
|
-
/** Build a fresh Mongoose schema from our NextMin schema definition. */
|
|
29
|
-
private buildMongooseSchema;
|
|
30
|
-
private mapAttribute;
|
|
31
|
-
private mapScalar;
|
|
32
|
-
/**
|
|
33
|
-
* Resolve (or lazily create) the compiled model for a schema.
|
|
34
|
-
* Uses schema.modelName as the Mongoose model name and
|
|
35
|
-
* (schema.collection ?? modelName).toLowerCase() as the collection.
|
|
36
|
-
*/
|
|
37
|
-
private getModel;
|
|
38
|
-
private getSchema;
|
|
39
|
-
private convertQueryFieldsToObjectId;
|
|
40
|
-
private toAppObject;
|
|
41
|
-
create(collection: string, data: any, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
|
|
42
|
-
read(collection: string, query: any, limit: number | undefined, skip: number | undefined, schemaDefinition: NextMinSchema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
|
|
43
|
-
count(collection: string, query: any, schemaDefinition: NextMinSchema): Promise<number>;
|
|
44
|
-
update(collection: string, id: string, data: any, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
|
|
45
|
-
delete(collection: string, id: string, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
|
|
46
|
-
private getNativeCollectionByModelName;
|
|
47
|
-
/** Create/drop single-field indexes based on schema plan.
|
|
48
|
-
* Only touches indexes named with the prefix below (won't touch user indexes).
|
|
49
|
-
*/
|
|
50
|
-
private managedIndexName;
|
|
51
|
-
syncIndexes(modelName: string, spec: FieldIndexSpec): Promise<void>;
|
|
52
|
-
}
|