@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.
Files changed (52) hide show
  1. package/README.md +48 -5
  2. package/dist/api/apiRouter.d.ts +10 -1
  3. package/dist/api/apiRouter.js +83 -22
  4. package/dist/api/router/mountBatchRoutes.d.ts +2 -0
  5. package/dist/api/router/mountBatchRoutes.js +90 -0
  6. package/dist/api/router/mountCrudRoutes.js +218 -223
  7. package/dist/api/router/mountFindRoutes.js +2 -49
  8. package/dist/api/router/mountSearchRoutes.js +10 -52
  9. package/dist/api/router/mountSearchRoutes_extended.js +7 -48
  10. package/dist/api/router/setupFileRoutes.js +41 -0
  11. package/dist/api/router/utils.d.ts +2 -0
  12. package/dist/api/router/utils.js +37 -8
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +83 -0
  15. package/dist/database/DatabaseAdapter.d.ts +8 -0
  16. package/dist/database/NMAdapter.d.ts +46 -0
  17. package/dist/database/NMAdapter.js +1243 -0
  18. package/dist/database/QueryEngine.d.ts +14 -0
  19. package/dist/database/QueryEngine.js +215 -0
  20. package/dist/database/utils.d.ts +2 -0
  21. package/dist/database/utils.js +21 -0
  22. package/dist/files/FileStorageAdapter.d.ts +5 -0
  23. package/dist/files/LocalFileStorageAdapter.d.ts +5 -0
  24. package/dist/files/LocalFileStorageAdapter.js +14 -0
  25. package/dist/files/S3FileStorageAdapter.d.ts +5 -0
  26. package/dist/files/S3FileStorageAdapter.js +14 -0
  27. package/dist/index.d.ts +4 -1
  28. package/dist/index.js +11 -5
  29. package/dist/models/BaseModel.d.ts +31 -2
  30. package/dist/models/BaseModel.js +33 -5
  31. package/dist/policy/authorize.js +95 -38
  32. package/dist/schemas/Users.json +66 -30
  33. package/dist/services/AggregateService.d.ts +8 -0
  34. package/dist/services/AggregateService.js +87 -0
  35. package/dist/services/RealtimeService.d.ts +20 -0
  36. package/dist/services/RealtimeService.js +93 -0
  37. package/dist/services/SchemaService.d.ts +3 -0
  38. package/dist/services/SchemaService.js +6 -2
  39. package/dist/utils/DefaultDataInitializer.js +10 -2
  40. package/dist/utils/Events.d.ts +34 -0
  41. package/dist/utils/Events.js +55 -0
  42. package/dist/utils/Logger.d.ts +2 -1
  43. package/dist/utils/Logger.js +20 -9
  44. package/dist/utils/QueryCache.d.ts +16 -0
  45. package/dist/utils/QueryCache.js +106 -0
  46. package/dist/utils/SchemaLoader.d.ts +6 -1
  47. package/dist/utils/SchemaLoader.js +49 -5
  48. package/package.json +19 -4
  49. package/dist/database/InMemoryAdapter.d.ts +0 -15
  50. package/dist/database/InMemoryAdapter.js +0 -71
  51. package/dist/database/MongoAdapter.d.ts +0 -52
  52. package/dist/database/MongoAdapter.js +0 -410
@@ -235,8 +235,16 @@ class DefaultDataInitializer {
235
235
  // Update only missing/empty fields (do not overwrite existing values)
236
236
  const updates = {};
237
237
  // apiKey
238
- if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
239
- this.apiKey = doc.apiKey; // honor existing
238
+ const envKey = process.env.NEXTMIN_API_KEY;
239
+ if (envKey && String(envKey).trim().length > 0) {
240
+ // If environment variable is set, it takes precedence over the database
241
+ this.apiKey = envKey;
242
+ if (doc.apiKey !== envKey) {
243
+ updates.apiKey = envKey;
244
+ }
245
+ }
246
+ else if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
247
+ this.apiKey = doc.apiKey; // honor existing if no env var
240
248
  }
241
249
  else {
242
250
  updates.apiKey = generatedApiKey;
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * NextMinEvents is a central event emitter for the NextMin ecosystem.
4
+ * It allows components to hook into CRUD and system events.
5
+ */
6
+ declare class NextMinEvents extends EventEmitter {
7
+ constructor();
8
+ /**
9
+ * Typed emit to provide better DX (Developer Experience)
10
+ */
11
+ emitEvent(event: string, payload: any): void;
12
+ }
13
+ export declare const events: NextMinEvents;
14
+ export declare const Events: {
15
+ BEFORE_CREATE: string;
16
+ AFTER_CREATE: string;
17
+ BEFORE_UPDATE: string;
18
+ AFTER_UPDATE: string;
19
+ BEFORE_DELETE: string;
20
+ AFTER_DELETE: string;
21
+ BEFORE_READ: string;
22
+ AFTER_READ: string;
23
+ AUTH_LOGIN: string;
24
+ AUTH_LOGOUT: string;
25
+ AUTH_SIGNUP: string;
26
+ SCHEMA_UPDATE: string;
27
+ SERVER_START: string;
28
+ SERVER_STOP: string;
29
+ };
30
+ /**
31
+ * Helper to generate model-specific event names
32
+ */
33
+ export declare function getModelEvent(modelName: string, action: 'create' | 'read' | 'update' | 'delete', phase?: 'before' | 'after'): string;
34
+ export {};
@@ -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.debug('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
+ }
@@ -1,8 +1,9 @@
1
1
  export declare class Logger {
2
- private isDevelopment;
2
+ private get isDevelopment();
3
3
  constructor();
4
4
  private getCallerInfo;
5
5
  private formatMessage;
6
+ debug(label: string, ...messages: any[]): void;
6
7
  log(label: string, ...messages: any[]): void;
7
8
  info(label: string, ...messages: any[]): void;
8
9
  warn(label: string, ...messages: any[]): void;
@@ -8,10 +8,11 @@ const kleur_1 = __importDefault(require("kleur"));
8
8
  const util_1 = require("util");
9
9
  const path_1 = __importDefault(require("path"));
10
10
  class Logger {
11
- constructor() {
12
- const env = process.env.NODE_ENV || 'development';
13
- this.isDevelopment = env === 'development';
11
+ get isDevelopment() {
12
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
13
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
14
14
  }
15
+ constructor() { }
15
16
  getCallerInfo() {
16
17
  const originalPrepareStackTrace = Error.prepareStackTrace;
17
18
  Error.prepareStackTrace = (err, stack) => stack;
@@ -24,12 +25,11 @@ class Logger {
24
25
  !frame.getFileName()?.includes('Logger.ts') &&
25
26
  !frame.getFunctionName()?.includes('getCallerInfo'));
26
27
  if (!callerFrame) {
27
- return 'unknown:0:0';
28
+ return 'unknown:0';
28
29
  }
29
- const fileName = path_1.default.relative(process.cwd(), callerFrame.getFileName() || 'unknown');
30
+ const fileName = path_1.default.basename(callerFrame.getFileName() || 'unknown');
30
31
  const lineNumber = callerFrame.getLineNumber() || 0;
31
- const columnNumber = callerFrame.getColumnNumber() || 0;
32
- return `${fileName}:${lineNumber}:${columnNumber}`;
32
+ return `${fileName}:${lineNumber}`;
33
33
  }
34
34
  formatMessage(level, label, messages) {
35
35
  const callerInfo = this.getCallerInfo();
@@ -47,12 +47,21 @@ class Logger {
47
47
  case 'ERROR':
48
48
  formattedLabel = kleur_1.default.red(`[ERROR] ${label} (${callerInfo}):`);
49
49
  break;
50
+ case 'DEBUG':
51
+ formattedLabel = kleur_1.default.magenta(`[DEBUG] ${label} (${callerInfo}):`);
52
+ break;
50
53
  default:
51
54
  formattedLabel = `[${level}] ${label} (${callerInfo}):`;
52
55
  }
53
56
  const formattedMessage = (0, util_1.format)(...messages);
54
57
  return `${formattedLabel} ${formattedMessage}`;
55
58
  }
59
+ debug(label, ...messages) {
60
+ if (this.isDevelopment) {
61
+ const formatted = this.formatMessage('DEBUG', label, messages);
62
+ console.debug(formatted);
63
+ }
64
+ }
56
65
  log(label, ...messages) {
57
66
  if (this.isDevelopment) {
58
67
  const formatted = this.formatMessage('LOG', label, messages);
@@ -66,8 +75,10 @@ class Logger {
66
75
  }
67
76
  }
68
77
  warn(label, ...messages) {
69
- const formatted = this.formatMessage('WARN', label, messages);
70
- console.warn(formatted);
78
+ if (this.isDevelopment) {
79
+ const formatted = this.formatMessage('WARN', label, messages);
80
+ console.warn(formatted);
81
+ }
71
82
  }
72
83
  error(label, ...messages) {
73
84
  const formatted = this.formatMessage('ERROR', label, messages);
@@ -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,13 +6,18 @@ 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;
12
17
  schemas: {
13
18
  [key: string]: Schema;
14
19
  };
15
- private isDevelopment;
20
+ private get isDevelopment();
16
21
  private nonOverridableSchemas;
17
22
  private userSchemasDir;
18
23
  private packageSchemasDir;
@@ -58,6 +58,10 @@ function validateSchema(obj) {
58
58
  return missing;
59
59
  }
60
60
  class SchemaLoader {
61
+ get isDevelopment() {
62
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
63
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
64
+ }
61
65
  // Singleton instance
62
66
  static getInstance() {
63
67
  if (!SchemaLoader._instance) {
@@ -68,10 +72,8 @@ class SchemaLoader {
68
72
  constructor() {
69
73
  this.emitter = new events_1.EventEmitter();
70
74
  this.schemas = {};
71
- this.isDevelopment = false;
72
75
  this.nonOverridableSchemas = new Set(['User', 'Role']);
73
76
  this.watcher = null; // <-- TS-safe
74
- this.isDevelopment = process.env.APP_MODE !== 'production';
75
77
  const baseDir = process.cwd();
76
78
  const basePackageDir = path_1.default.resolve(__dirname, '..');
77
79
  this.packageSchemasDir = path_1.default.join(basePackageDir, 'schemas');
@@ -138,13 +140,14 @@ class SchemaLoader {
138
140
  continue;
139
141
  if (Object.prototype.hasOwnProperty.call(mergedAttrs, key)) {
140
142
  const val = mergedAttrs[key];
143
+ const isInherited = !Object.prototype.hasOwnProperty.call(schema.attributes, key);
141
144
  if (Array.isArray(val) && val[0] && typeof val[0] === 'object') {
142
145
  const { unique: _u, index: _i, ...rest } = val[0];
143
- mergedAttrs[key] = [{ ...rest }];
146
+ mergedAttrs[key] = [{ ...rest, inherited: isInherited }];
144
147
  }
145
148
  else if (val && typeof val === 'object') {
146
149
  const { unique: _u, index: _i, ...rest } = val;
147
- mergedAttrs[key] = { ...rest };
150
+ mergedAttrs[key] = { ...rest, inherited: isInherited };
148
151
  }
149
152
  }
150
153
  }
@@ -153,14 +156,50 @@ class SchemaLoader {
153
156
  ...baseSchema.allowedMethods,
154
157
  ...schema.allowedMethods,
155
158
  };
159
+ // Merge access (policies/masks/reachability)
160
+ if (baseSchema.access) {
161
+ const baseAccess = baseSchema.access;
162
+ if (!schema.access)
163
+ schema.access = {};
164
+ // 1. Selective Merge for bypassPrivacy (combine roles)
165
+ if (baseAccess.bypassPrivacy?.roles) {
166
+ if (!schema.access.bypassPrivacy)
167
+ schema.access.bypassPrivacy = {};
168
+ const baseRoles = baseAccess.bypassPrivacy.roles;
169
+ const currentRoles = schema.access.bypassPrivacy.roles || [];
170
+ schema.access.bypassPrivacy.roles = Array.from(new Set([...baseRoles, ...currentRoles]));
171
+ }
172
+ // 2. Inherit masks/filters/restrictions ONLY if not defined in child
173
+ // Note: We DO NOT inherit public/authenticated/roles reachability here.
174
+ // Child schemas should manage their own reachability.
175
+ if (!schema.access.readMask)
176
+ schema.access.readMask = baseAccess.readMask;
177
+ if (!schema.access.writeDeny)
178
+ schema.access.writeDeny = baseAccess.writeDeny;
179
+ if (!schema.access.queryFilter)
180
+ schema.access.queryFilter = baseAccess.queryFilter;
181
+ if (!schema.access.createDefaults)
182
+ schema.access.createDefaults = baseAccess.createDefaults;
183
+ if (!schema.access.restrictions)
184
+ schema.access.restrictions = baseAccess.restrictions;
185
+ if (!schema.access.conditions)
186
+ schema.access.conditions = baseAccess.conditions;
187
+ }
156
188
  // Inject hidden link field to base (used for storage join)
157
189
  // Ensure hidden link field
158
190
  const linkField = 'baseId';
159
191
  if (!schema.attributes[linkField]) {
192
+ const isUserExt = baseName.toLowerCase() === 'users';
160
193
  schema.attributes[linkField] = {
161
194
  type: 'ObjectId',
162
195
  ref: baseName,
163
- private: true,
196
+ private: !isUserExt,
197
+ required: false,
198
+ label: isUserExt ? 'Associate User Account (Optional)' : 'Base Record',
199
+ show: isUserExt ? 'username' : 'id',
200
+ index: true,
201
+ unique: isUserExt,
202
+ sparse: isUserExt,
164
203
  };
165
204
  }
166
205
  }
@@ -303,6 +342,11 @@ class SchemaLoader {
303
342
  showCount: s.showCount,
304
343
  allowedMethods: s.allowedMethods,
305
344
  attributes: attributesWithTimestamps,
345
+ extends: s.extends,
346
+ group: s.group,
347
+ schemaType: s.schemaType,
348
+ columnsSelector: s.columnsSelector,
349
+ actions: s.actions,
306
350
  };
307
351
  }
308
352
  return out;
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "1.4.6",
3
+ "version": "2.0.2",
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
- "mongoose": "^8.17.0",
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
- "socket.io": "^4.7.5"
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
- "typescript": "^5.3.3"
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
- }