@airoom/nextmin-node 1.4.6 → 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.
Files changed (40) hide show
  1. package/README.md +48 -5
  2. package/dist/api/apiRouter.d.ts +2 -0
  3. package/dist/api/apiRouter.js +67 -21
  4. package/dist/api/router/mountCrudRoutes.js +207 -220
  5. package/dist/api/router/mountFindRoutes.js +2 -49
  6. package/dist/api/router/mountSearchRoutes.js +10 -52
  7. package/dist/api/router/mountSearchRoutes_extended.js +7 -48
  8. package/dist/api/router/utils.js +20 -7
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.js +83 -0
  11. package/dist/database/DatabaseAdapter.d.ts +7 -0
  12. package/dist/database/NMAdapter.d.ts +41 -0
  13. package/dist/database/NMAdapter.js +979 -0
  14. package/dist/database/QueryEngine.d.ts +14 -0
  15. package/dist/database/QueryEngine.js +215 -0
  16. package/dist/database/utils.d.ts +2 -0
  17. package/dist/database/utils.js +21 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.js +11 -5
  20. package/dist/models/BaseModel.d.ts +16 -0
  21. package/dist/models/BaseModel.js +32 -4
  22. package/dist/policy/authorize.js +95 -38
  23. package/dist/schemas/Users.json +66 -30
  24. package/dist/services/RealtimeService.d.ts +20 -0
  25. package/dist/services/RealtimeService.js +93 -0
  26. package/dist/services/SchemaService.d.ts +3 -0
  27. package/dist/services/SchemaService.js +6 -2
  28. package/dist/utils/DefaultDataInitializer.js +10 -2
  29. package/dist/utils/Events.d.ts +34 -0
  30. package/dist/utils/Events.js +55 -0
  31. package/dist/utils/Logger.js +12 -10
  32. package/dist/utils/QueryCache.d.ts +16 -0
  33. package/dist/utils/QueryCache.js +106 -0
  34. package/dist/utils/SchemaLoader.d.ts +5 -0
  35. package/dist/utils/SchemaLoader.js +45 -3
  36. package/package.json +19 -4
  37. package/dist/database/InMemoryAdapter.d.ts +0 -15
  38. package/dist/database/InMemoryAdapter.js +0 -71
  39. package/dist/database/MongoAdapter.d.ts +0 -52
  40. package/dist/database/MongoAdapter.js +0 -410
@@ -0,0 +1,14 @@
1
+ import { Schema } from '../models/BaseModel';
2
+ /**
3
+ * QueryEngine is responsible for parsing and transforming complex filters
4
+ * into a format that DatabaseAdapters can understand.
5
+ * It also handles backward compatibility for legacy query parameters.
6
+ */
7
+ export declare class QueryEngine {
8
+ /**
9
+ * Parses legacy query parameters and complex 'where' filters into a unified filter object.
10
+ */
11
+ static parse(reqQuery: any, schema: Schema): any;
12
+ private static getPredicate;
13
+ private static coerceValue;
14
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryEngine = void 0;
4
+ const utils_1 = require("../api/router/utils");
5
+ /**
6
+ * QueryEngine is responsible for parsing and transforming complex filters
7
+ * into a format that DatabaseAdapters can understand.
8
+ * It also handles backward compatibility for legacy query parameters.
9
+ */
10
+ class QueryEngine {
11
+ /**
12
+ * Parses legacy query parameters and complex 'where' filters into a unified filter object.
13
+ */
14
+ static parse(reqQuery, schema) {
15
+ let filter = {};
16
+ const RESERVED = new Set([
17
+ 'page', 'limit',
18
+ 'q', 'searchKey', 'searchKeys', 'searchMode', 'searchType', // Legacy Search
19
+ 'dateFrom', 'dateTo', 'dateKey', // Legacy Date Filtering
20
+ 'sort', 'sortType', 'fields', 'where'
21
+ ]);
22
+ // 1. Handle Complex 'where' filter (New Architecture)
23
+ if (reqQuery.where) {
24
+ try {
25
+ const where = typeof reqQuery.where === 'string' ? JSON.parse(reqQuery.where) : reqQuery.where;
26
+ if ((0, utils_1.isPlainObject)(where)) {
27
+ Object.assign(filter, where);
28
+ }
29
+ }
30
+ catch (err) {
31
+ // Ignore invalid JSON in 'where'
32
+ }
33
+ }
34
+ // 2. Handle Legacy Global Search (q)
35
+ const q = String(reqQuery.q ?? '').trim();
36
+ if (q) {
37
+ const terms = (0, utils_1.splitCSV)(q);
38
+ const searchKey = String(reqQuery.searchKey ?? '').trim();
39
+ const searchKeysCSV = String(reqQuery.searchKeys ?? '').trim();
40
+ const searchMode = /^(and|or)$/i.test(String(reqQuery.searchMode ?? ''))
41
+ ? String(reqQuery.searchMode).toLowerCase()
42
+ : 'or';
43
+ const searchType = String(reqQuery.searchType ?? 'starts').toLowerCase();
44
+ const keys = searchKey ? [searchKey] : (searchKeysCSV ? (0, utils_1.splitCSV)(searchKeysCSV) : Object.keys(schema.attributes));
45
+ if (searchMode === 'and') {
46
+ const termPreds = terms.map(t => {
47
+ const keyors = keys.map(k => this.getPredicate(k, t, schema, searchType)).filter(Boolean);
48
+ if (keyors.length === 0)
49
+ return null;
50
+ if (keyors.length === 1)
51
+ return keyors[0];
52
+ return { $or: keyors };
53
+ }).filter(Boolean);
54
+ if (termPreds.length === 1) {
55
+ Object.assign(filter, termPreds[0]);
56
+ }
57
+ else if (termPreds.length > 1) {
58
+ // Try to merge into root if no keys collide and no $ operators at top-level
59
+ let canMerge = true;
60
+ const allKeys = new Set();
61
+ for (const p of termPreds) {
62
+ const pk = Object.keys(p);
63
+ if (pk.length !== 1 || pk[0].startsWith('$') || allKeys.has(pk[0])) {
64
+ canMerge = false;
65
+ break;
66
+ }
67
+ allKeys.add(pk[0]);
68
+ }
69
+ if (canMerge) {
70
+ termPreds.forEach(p => Object.assign(filter, p));
71
+ }
72
+ else {
73
+ filter.$and = [...(filter.$and || []), ...termPreds];
74
+ }
75
+ }
76
+ }
77
+ else {
78
+ const ors = [];
79
+ terms.forEach(t => {
80
+ keys.forEach(k => {
81
+ const p = this.getPredicate(k, t, schema, searchType);
82
+ if (p)
83
+ ors.push(p);
84
+ });
85
+ });
86
+ if (ors.length) {
87
+ if (ors.length === 1)
88
+ Object.assign(filter, ors[0]);
89
+ else
90
+ filter.$or = [...(filter.$or || []), ...ors];
91
+ }
92
+ }
93
+ }
94
+ // 3. Handle Legacy Date Filters
95
+ const dateFrom = reqQuery.dateFrom;
96
+ const dateTo = reqQuery.dateTo;
97
+ if (dateFrom || dateTo) {
98
+ const dateKey = String(reqQuery.dateKey ?? 'createdAt').trim();
99
+ if (schema.attributes[dateKey]) {
100
+ const range = {};
101
+ if (dateFrom)
102
+ range.$gte = new Date(dateFrom);
103
+ if (dateTo)
104
+ range.$lte = new Date(dateTo);
105
+ filter[dateKey] = range;
106
+ }
107
+ }
108
+ // 4. Handle Legacy Shallow Field Filters
109
+ for (const [k, vRaw] of Object.entries(reqQuery)) {
110
+ if (RESERVED.has(k))
111
+ continue;
112
+ let effectiveKey = k;
113
+ let attr = schema.attributes[k];
114
+ // Support dotted paths in shallow filters (e.g. specialities.id)
115
+ if (!attr && k.includes('.')) {
116
+ const parts = k.split('.');
117
+ const root = parts[0];
118
+ const rootAttr = schema.attributes[root];
119
+ if (rootAttr) {
120
+ const a = Array.isArray(rootAttr) ? rootAttr[0] : rootAttr;
121
+ if (a.ref && (parts[parts.length - 1] === 'id' || parts[parts.length - 1] === '_id')) {
122
+ effectiveKey = root;
123
+ attr = rootAttr;
124
+ }
125
+ }
126
+ }
127
+ if (!attr)
128
+ continue;
129
+ const v = Array.isArray(vRaw) ? vRaw.map(String).join(',') : String(vRaw);
130
+ if (!v?.length)
131
+ continue;
132
+ if (v.includes(',')) {
133
+ filter[effectiveKey] = { $in: v.split(',').map(s => this.coerceValue(s.trim())) };
134
+ }
135
+ else if (v.startsWith('!')) {
136
+ filter[effectiveKey] = { $ne: this.coerceValue(v.slice(1)) };
137
+ }
138
+ else if (/^(gt|gte|lt|lte):/i.test(v)) {
139
+ const [op, rest] = v.split(':', 2);
140
+ const map = { gt: '$gt', gte: '$gte', lt: '$lt', lte: '$lte' };
141
+ filter[effectiveKey] = { [map[op.toLowerCase()]]: this.coerceValue(rest) };
142
+ }
143
+ else {
144
+ filter[effectiveKey] = this.coerceValue(v);
145
+ }
146
+ }
147
+ return filter;
148
+ }
149
+ static getPredicate(key, value, schema, searchType = 'starts') {
150
+ let effectiveKey = key;
151
+ // Handle ModelName.field prefix
152
+ if (effectiveKey.includes('.')) {
153
+ const parts = effectiveKey.split('.');
154
+ if (parts[0].toLowerCase() === schema.modelName.toLowerCase()) {
155
+ effectiveKey = parts.slice(1).join('.');
156
+ }
157
+ }
158
+ // Support direct id/_id search
159
+ if (effectiveKey === 'id' || effectiveKey === '_id') {
160
+ return { [effectiveKey]: value };
161
+ }
162
+ // Handle dotted paths for relations (e.g. specialities.id -> specialities: value)
163
+ if (effectiveKey.includes('.')) {
164
+ const [root, ...rest] = effectiveKey.split('.');
165
+ const attr = schema.attributes[root];
166
+ if (attr) {
167
+ const a = Array.isArray(attr) ? attr[0] : attr;
168
+ const suffix = rest.join('.');
169
+ if (a.ref && (suffix === 'id' || suffix === '_id')) {
170
+ return { [root]: value };
171
+ }
172
+ }
173
+ }
174
+ const attr = schema.attributes[effectiveKey];
175
+ if (!attr)
176
+ return null;
177
+ const type = (0, utils_1.normalizeAttrType)(attr);
178
+ // Handle reference fields with searchBy or show
179
+ const a = Array.isArray(attr) ? attr[0] : attr;
180
+ if (a.ref && (a.searchBy || a.show)) {
181
+ return {
182
+ [effectiveKey]: {
183
+ $nestedSearch: {
184
+ ref: a.ref,
185
+ searchBy: a.searchBy,
186
+ show: a.show,
187
+ value: value
188
+ }
189
+ }
190
+ };
191
+ }
192
+ if (type === 'string' || type === 'json' || type === 'array' || type === 'object') {
193
+ const anchoredValue = (searchType === 'includes' || value.startsWith('^')) ? value : `^${value}`;
194
+ return { [effectiveKey]: { $regex: anchoredValue, $options: 'i' } };
195
+ }
196
+ else if (type === 'objectid') {
197
+ return { [effectiveKey]: value };
198
+ }
199
+ else if (type === 'number' && !isNaN(Number(value))) {
200
+ return { [effectiveKey]: Number(value) };
201
+ }
202
+ return null;
203
+ }
204
+ static coerceValue(raw) {
205
+ const s = String(raw).trim();
206
+ if (/^(true|false)$/i.test(s))
207
+ return /^true$/i.test(s);
208
+ if (/^-?\d+(\.\d+)?$/.test(s))
209
+ return Number(s);
210
+ if (/^null$/i.test(s))
211
+ return null;
212
+ return s;
213
+ }
214
+ }
215
+ exports.QueryEngine = QueryEngine;
@@ -0,0 +1,2 @@
1
+ import { Schema } from '../models/BaseModel';
2
+ export declare function isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: Schema | undefined): boolean;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isExclusiveProjection = isExclusiveProjection;
4
+ function isExclusiveProjection(projection, schema) {
5
+ if (!projection)
6
+ return false;
7
+ return Object.keys(projection).some(k => {
8
+ if (k.includes('.') || k === 'id' || k === '_id' || k === 'baseId' || k === 'exId' || k === '__childId')
9
+ return false;
10
+ if (schema) {
11
+ const attr = schema.attributes[k];
12
+ if (!attr)
13
+ return true; // Root field not in attributes (e.g. fullName) -> Exclusive mode
14
+ const a = Array.isArray(attr) ? attr[0] : attr;
15
+ const isRelation = String(a?.type || '').toLowerCase() === 'objectid';
16
+ if (isRelation)
17
+ return false;
18
+ }
19
+ return true;
20
+ });
21
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { APIRouterOptions } from './api/apiRouter';
2
- export { MongoAdapter } from './database/MongoAdapter';
2
+ /** @deprecated Use NMAdapter instead */
3
+ export { NMAdapter } from './database/NMAdapter';
4
+ export { QueryEngine } from './database/QueryEngine';
5
+ export { events, Events } from './utils/Events';
3
6
  export { generateApiKey } from './utils/apiKey';
4
7
  export * from './files/FileStorageAdapter';
5
8
  export * from './files/S3FileStorageAdapter';
package/dist/index.js CHANGED
@@ -17,13 +17,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.generateApiKey = exports.MongoAdapter = void 0;
20
+ exports.generateApiKey = exports.Events = exports.events = exports.QueryEngine = exports.NMAdapter = void 0;
21
21
  exports.createNextMinRouter = createNextMinRouter;
22
22
  const express_1 = __importDefault(require("express"));
23
23
  const body_parser_1 = __importDefault(require("body-parser"));
24
24
  const apiRouter_1 = require("./api/apiRouter");
25
- var MongoAdapter_1 = require("./database/MongoAdapter");
26
- Object.defineProperty(exports, "MongoAdapter", { enumerable: true, get: function () { return MongoAdapter_1.MongoAdapter; } });
25
+ /** @deprecated Use NMAdapter instead */
26
+ var NMAdapter_1 = require("./database/NMAdapter");
27
+ Object.defineProperty(exports, "NMAdapter", { enumerable: true, get: function () { return NMAdapter_1.NMAdapter; } });
28
+ var QueryEngine_1 = require("./database/QueryEngine");
29
+ Object.defineProperty(exports, "QueryEngine", { enumerable: true, get: function () { return QueryEngine_1.QueryEngine; } });
30
+ var Events_1 = require("./utils/Events");
31
+ Object.defineProperty(exports, "events", { enumerable: true, get: function () { return Events_1.events; } });
32
+ Object.defineProperty(exports, "Events", { enumerable: true, get: function () { return Events_1.Events; } });
27
33
  var apiKey_1 = require("./utils/apiKey");
28
34
  Object.defineProperty(exports, "generateApiKey", { enumerable: true, get: function () { return apiKey_1.generateApiKey; } });
29
35
  __exportStar(require("./files/FileStorageAdapter"), exports);
@@ -65,8 +71,8 @@ function createNextMinRouter(options) {
65
71
  // Allow non-mutating methods and preflight
66
72
  if (!['POST', 'PUT', 'PATCH'].includes(req.method))
67
73
  return next();
68
- // Never block /files (multer needs to see raw multipart)
69
- if (req.path.startsWith('/files') || isMultipart(req))
74
+ // Never block /files (multer needs to see raw multipart) or /_cleanup (trigger action)
75
+ if (req.path.startsWith('/files') || req.path === '/_cleanup' || isMultipart(req))
70
76
  return next();
71
77
  // Enforce body presence only for JSON or urlencoded
72
78
  const mustHaveBody = isJson(req) || isUrlEncoded(req);
@@ -4,6 +4,7 @@ export type SortSpec = Record<string, SortDir>;
4
4
  export type ReadOptions = {
5
5
  sort?: SortSpec;
6
6
  projection?: Record<string, 0 | 1>;
7
+ depth?: number;
7
8
  };
8
9
  export interface Attributes {
9
10
  type: string;
@@ -14,9 +15,21 @@ export interface Attributes {
14
15
  select?: string[];
15
16
  private?: boolean;
16
17
  unique?: boolean;
18
+ sparse?: boolean;
17
19
  sensitive?: boolean;
18
20
  writeOnly?: boolean;
19
21
  rich?: boolean;
22
+ searchBy?: string;
23
+ show?: string;
24
+ }
25
+ export interface CustomAction {
26
+ label: string;
27
+ href: string;
28
+ icon?: string;
29
+ variant?: 'light' | 'flat' | 'solid' | 'bordered';
30
+ color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
31
+ relatedModel?: string;
32
+ relatedField?: string;
20
33
  }
21
34
  export interface Schema {
22
35
  modelName: string;
@@ -32,6 +45,8 @@ export interface Schema {
32
45
  delete?: boolean;
33
46
  };
34
47
  extends?: string;
48
+ actions?: CustomAction[];
49
+ access?: any;
35
50
  }
36
51
  export declare class BaseModel {
37
52
  private schema;
@@ -43,4 +58,5 @@ export declare class BaseModel {
43
58
  update(id: string, data: any, includePrivateFields?: boolean): Promise<any>;
44
59
  delete(id: string, includePrivateFields?: boolean): Promise<any>;
45
60
  count(query: any, includePrivateFields?: boolean): Promise<number>;
61
+ findFirstRelatedIds(field: string, ids: string[]): Promise<Map<string, string>>;
46
62
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseModel = void 0;
4
+ const Events_1 = require("../utils/Events");
4
5
  class BaseModel {
5
6
  constructor(schema, adapter) {
6
7
  this.schema = schema;
@@ -8,24 +9,51 @@ class BaseModel {
8
9
  this.collectionName = schema.modelName.toLowerCase();
9
10
  }
10
11
  async create(data, includePrivateFields) {
12
+ const payload = { modelName: this.schema.modelName, data };
13
+ Events_1.events.emitEvent(Events_1.Events.BEFORE_CREATE, payload);
14
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'create', 'before'), payload);
11
15
  for (const [key, attr] of Object.entries(this.schema.attributes)) {
12
16
  if (data[key] === undefined && attr.default !== undefined) {
13
17
  data[key] = attr.default;
14
18
  }
15
19
  }
16
- return await this.adapter.create(this.collectionName, data, this.schema, includePrivateFields);
20
+ const result = await this.adapter.create(this.collectionName, data, this.schema, includePrivateFields);
21
+ Events_1.events.emitEvent(Events_1.Events.AFTER_CREATE, { ...payload, result });
22
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'create', 'after'), { ...payload, result });
23
+ return result;
17
24
  }
18
25
  async read(query, limit, skip, includePrivateFields, options) {
19
- return await this.adapter.read(this.collectionName, query, limit, skip, this.schema, includePrivateFields, options);
26
+ const payload = { modelName: this.schema.modelName, query, limit, skip, options };
27
+ Events_1.events.emitEvent(Events_1.Events.BEFORE_READ, payload);
28
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'read', 'before'), payload);
29
+ const result = await this.adapter.read(this.collectionName, query, limit, skip, this.schema, includePrivateFields, options);
30
+ Events_1.events.emitEvent(Events_1.Events.AFTER_READ, { ...payload, result });
31
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'read', 'after'), { ...payload, result });
32
+ return result;
20
33
  }
21
34
  async update(id, data, includePrivateFields) {
22
- return await this.adapter.update(this.collectionName, id, data, this.schema, includePrivateFields);
35
+ const payload = { modelName: this.schema.modelName, id, data };
36
+ Events_1.events.emitEvent(Events_1.Events.BEFORE_UPDATE, payload);
37
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'update', 'before'), payload);
38
+ const result = await this.adapter.update(this.collectionName, id, data, this.schema, includePrivateFields);
39
+ Events_1.events.emitEvent(Events_1.Events.AFTER_UPDATE, { ...payload, result });
40
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'update', 'after'), { ...payload, result });
41
+ return result;
23
42
  }
24
43
  async delete(id, includePrivateFields) {
25
- return await this.adapter.delete(this.collectionName, id, this.schema, includePrivateFields);
44
+ const payload = { modelName: this.schema.modelName, id };
45
+ Events_1.events.emitEvent(Events_1.Events.BEFORE_DELETE, payload);
46
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'delete', 'before'), payload);
47
+ const result = await this.adapter.delete(this.collectionName, id, this.schema, includePrivateFields);
48
+ Events_1.events.emitEvent(Events_1.Events.AFTER_DELETE, { ...payload, result });
49
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'delete', 'after'), { ...payload, result });
50
+ return result;
26
51
  }
27
52
  async count(query, includePrivateFields) {
28
53
  return await this.adapter.count(this.collectionName, query, this.schema, includePrivateFields);
29
54
  }
55
+ async findFirstRelatedIds(field, ids) {
56
+ return await this.adapter.findFirstRelatedIds(this.collectionName, field, ids);
57
+ }
30
58
  }
31
59
  exports.BaseModel = BaseModel;