@airoom/nextmin-node 0.1.0

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 (45) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +178 -0
  3. package/dist/api/apiRouter.d.ts +65 -0
  4. package/dist/api/apiRouter.js +1548 -0
  5. package/dist/database/DatabaseAdapter.d.ts +14 -0
  6. package/dist/database/DatabaseAdapter.js +2 -0
  7. package/dist/database/InMemoryAdapter.d.ts +15 -0
  8. package/dist/database/InMemoryAdapter.js +71 -0
  9. package/dist/database/MongoAdapter.d.ts +52 -0
  10. package/dist/database/MongoAdapter.js +409 -0
  11. package/dist/files/FileStorageAdapter.d.ts +35 -0
  12. package/dist/files/FileStorageAdapter.js +2 -0
  13. package/dist/files/S3FileStorageAdapter.d.ts +30 -0
  14. package/dist/files/S3FileStorageAdapter.js +84 -0
  15. package/dist/files/filename.d.ts +5 -0
  16. package/dist/files/filename.js +40 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +87 -0
  19. package/dist/models/BaseModel.d.ts +44 -0
  20. package/dist/models/BaseModel.js +31 -0
  21. package/dist/policy/authorize.d.ts +25 -0
  22. package/dist/policy/authorize.js +305 -0
  23. package/dist/policy/conditions.d.ts +14 -0
  24. package/dist/policy/conditions.js +30 -0
  25. package/dist/policy/types.d.ts +53 -0
  26. package/dist/policy/types.js +2 -0
  27. package/dist/policy/utils.d.ts +9 -0
  28. package/dist/policy/utils.js +118 -0
  29. package/dist/schemas/Roles.json +64 -0
  30. package/dist/schemas/Settings.json +62 -0
  31. package/dist/schemas/Users.json +123 -0
  32. package/dist/services/SchemaService.d.ts +10 -0
  33. package/dist/services/SchemaService.js +46 -0
  34. package/dist/utils/DefaultDataInitializer.d.ts +21 -0
  35. package/dist/utils/DefaultDataInitializer.js +269 -0
  36. package/dist/utils/Logger.d.ts +12 -0
  37. package/dist/utils/Logger.js +79 -0
  38. package/dist/utils/SchemaLoader.d.ts +51 -0
  39. package/dist/utils/SchemaLoader.js +323 -0
  40. package/dist/utils/apiKey.d.ts +5 -0
  41. package/dist/utils/apiKey.js +14 -0
  42. package/dist/utils/fieldCodecs.d.ts +13 -0
  43. package/dist/utils/fieldCodecs.js +133 -0
  44. package/package.json +45 -0
  45. package/tsconfig.json +8 -0
@@ -0,0 +1,269 @@
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.DefaultDataInitializer = void 0;
7
+ const Logger_1 = __importDefault(require("./Logger"));
8
+ const apiKey_1 = require("./apiKey");
9
+ const bcrypt_1 = __importDefault(require("bcrypt"));
10
+ const DEFAULT_ROLES = [
11
+ {
12
+ name: 'superadmin',
13
+ description: 'Super administrator with all privileges',
14
+ type: 'system',
15
+ },
16
+ {
17
+ name: 'admin',
18
+ description: 'Administrator with elevated privileges',
19
+ type: 'system',
20
+ },
21
+ {
22
+ name: 'user',
23
+ description: 'Regular user with limited privileges',
24
+ type: 'system',
25
+ },
26
+ ];
27
+ // Keep your env override as requested
28
+ const DEFAULT_SITE_NAME = process.env.siteName || 'NextMin';
29
+ const DEFAULT_SITE_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="512" height="128" viewBox="0 0 512 128" role="img" aria-label="NextMin">
30
+ <rect width="100%" height="100%" rx="16" ry="16" fill="#0B1220"/>
31
+ <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
32
+ font-family="Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial"
33
+ font-size="48" font-weight="700" letter-spacing="1">
34
+ <tspan fill="#22d3ee">Next</tspan><tspan fill="#a78bfa">Min</tspan>
35
+ </text>
36
+ </svg>`;
37
+ class DefaultDataInitializer {
38
+ constructor(dbAdapter, models, opts) {
39
+ this.dbAdapter = dbAdapter;
40
+ this.models = models;
41
+ this.apiKey = null;
42
+ this.jwtSecret =
43
+ opts?.jwtSecret ?? (process.env.JWT_SECRET || 'default_jwt_secret');
44
+ }
45
+ getApiKey() {
46
+ return this.apiKey;
47
+ }
48
+ async initialize() {
49
+ await this.ensureRoles();
50
+ await this.ensureSuperAdminUser();
51
+ await this.ensureSettingsDocument(); // <-- unified for new schema
52
+ }
53
+ /* ---------------- helpers ---------------- */
54
+ getModel(...candidates) {
55
+ for (const raw of candidates) {
56
+ const key = raw.toLowerCase();
57
+ if (this.models[key])
58
+ return this.models[key];
59
+ }
60
+ for (const key of Object.keys(this.models)) {
61
+ if (candidates.some((c) => key === c.toLowerCase() || key.startsWith(c.toLowerCase()))) {
62
+ return this.models[key];
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ inferUserRoleStorage(userModel) {
68
+ try {
69
+ const roleAttr = userModel?.schema?.attributes?.role;
70
+ const a = Array.isArray(roleAttr) ? roleAttr?.[0] : roleAttr;
71
+ let t = a?.type ?? a;
72
+ if (typeof t === 'function' && t.name)
73
+ t = t.name;
74
+ if (t && typeof t === 'object' && 'name' in t)
75
+ t = t.name;
76
+ if (typeof t === 'string') {
77
+ const tl = t.toLowerCase();
78
+ if (tl === 'string')
79
+ return 'string';
80
+ if (tl === 'objectid' || tl === 'oid' || tl === 'ref')
81
+ return 'ref';
82
+ }
83
+ }
84
+ catch { }
85
+ return 'string';
86
+ }
87
+ async ensureHash(plain) {
88
+ const salt = await bcrypt_1.default.genSalt(10);
89
+ return bcrypt_1.default.hash(plain + this.jwtSecret, salt);
90
+ }
91
+ /* ---------------- roles ---------------- */
92
+ async ensureRoles() {
93
+ const roleModel = this.getModel('roles', 'role');
94
+ if (!roleModel) {
95
+ Logger_1.default.warn('DefaultDataInitializer', 'Roles model not found, skipping roles initialization.');
96
+ return;
97
+ }
98
+ for (const role of DEFAULT_ROLES) {
99
+ const exists = await roleModel.read({ name: role.name }, 1, 0);
100
+ if (!exists?.length) {
101
+ try {
102
+ await roleModel.create({ ...role });
103
+ Logger_1.default.info('DefaultDataInitializer', `Role created: ${role.name}`);
104
+ }
105
+ catch (err) {
106
+ Logger_1.default.error('DefaultDataInitializer', `Failed to create role ${role.name}:`, err);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ /* ---------------- superadmin user ---------------- */
112
+ async ensureSuperAdminUser() {
113
+ const userModel = this.getModel('users', 'user');
114
+ if (!userModel) {
115
+ Logger_1.default.warn('DefaultDataInitializer', 'Users model not found, skipping superadmin init.');
116
+ return;
117
+ }
118
+ const roleModel = this.getModel('roles', 'role');
119
+ const roleStorage = this.inferUserRoleStorage(userModel);
120
+ let superRoleId = null;
121
+ if (roleStorage === 'ref') {
122
+ if (!roleModel) {
123
+ Logger_1.default.error('DefaultDataInitializer', 'Roles model missing; cannot seed super user with ref role.');
124
+ return;
125
+ }
126
+ const superRole = (await roleModel.read({ name: 'superadmin' }, 1, 0))?.[0];
127
+ superRoleId = superRole?.id ?? superRole?._id ?? null;
128
+ if (!superRoleId) {
129
+ Logger_1.default.error('DefaultDataInitializer', 'Superadmin role missing; cannot seed super user (ref role).');
130
+ return;
131
+ }
132
+ }
133
+ const email = 'super@example.com';
134
+ const username = 'superadmin';
135
+ const lower = email.toLowerCase();
136
+ const foundByEmail = (await userModel.read({ email: lower }, 1, 0, true))?.[0];
137
+ const foundByUsername = (await userModel.read({ username }, 1, 0, true))?.[0];
138
+ const user = foundByEmail || foundByUsername;
139
+ const roleValue = roleStorage === 'ref' ? superRoleId : 'superadmin';
140
+ if (!user) {
141
+ const password = await this.ensureHash('supersecurepassword');
142
+ const payload = {
143
+ username,
144
+ email: lower,
145
+ firstName: 'Super',
146
+ lastName: 'Admin',
147
+ password,
148
+ role: roleValue,
149
+ status: 'active',
150
+ type: 'system',
151
+ createdAt: new Date().toISOString(),
152
+ };
153
+ try {
154
+ await userModel.create(payload);
155
+ Logger_1.default.info('DefaultDataInitializer', 'Super admin user created.');
156
+ }
157
+ catch (err) {
158
+ Logger_1.default.error('DefaultDataInitializer', 'Failed to create super admin user:', err);
159
+ }
160
+ return;
161
+ }
162
+ const updates = {};
163
+ if (user.email !== lower)
164
+ updates.email = lower;
165
+ const existingRoleRaw = user.role;
166
+ const existingRoleName = typeof existingRoleRaw === 'string'
167
+ ? existingRoleRaw
168
+ : (existingRoleRaw?.name ?? null);
169
+ const existingRoleId = typeof existingRoleRaw === 'object'
170
+ ? (existingRoleRaw?.id ?? existingRoleRaw?._id ?? null)
171
+ : null;
172
+ const hasCorrectRole = roleStorage === 'string'
173
+ ? existingRoleRaw === 'superadmin' || existingRoleName === 'superadmin'
174
+ : existingRoleId === superRoleId || existingRoleRaw === superRoleId;
175
+ if (!hasCorrectRole)
176
+ updates.role = roleValue;
177
+ if (user.status !== 'active')
178
+ updates.status = 'active';
179
+ const ok = user.password
180
+ ? await bcrypt_1.default.compare('supersecurepassword' + this.jwtSecret, user.password)
181
+ : false;
182
+ if (!ok) {
183
+ updates.password = await this.ensureHash('supersecurepassword');
184
+ }
185
+ if (Object.keys(updates).length) {
186
+ try {
187
+ await userModel.update(user.id ?? user._id, updates);
188
+ Logger_1.default.info('DefaultDataInitializer', 'Super admin user normalized/updated.');
189
+ }
190
+ catch (err) {
191
+ Logger_1.default.error('DefaultDataInitializer', 'Failed to update super admin user:', err);
192
+ }
193
+ }
194
+ else {
195
+ Logger_1.default.info('DefaultDataInitializer', 'Super admin user already OK.');
196
+ }
197
+ }
198
+ /* ---------------- unified Settings (new schema) ---------------- */
199
+ async ensureSettingsDocument() {
200
+ const settingsModel = this.getModel('settings', 'setting');
201
+ if (!settingsModel) {
202
+ Logger_1.default.warn('DefaultDataInitializer', 'Settings model not found; skipping settings initialization.');
203
+ return;
204
+ }
205
+ // Read (singleton)
206
+ let doc = (await settingsModel.read({}, 1, 0, true))?.[0];
207
+ // Figure out desired values
208
+ const generatedApiKey = process.env.NEXTMIN_API_KEY || (0, apiKey_1.generateApiKey)();
209
+ const desiredSiteName = DEFAULT_SITE_NAME;
210
+ const desiredSiteLogoArr = [DEFAULT_SITE_LOGO_SVG]; // schema says array of string
211
+ // Create if missing
212
+ if (!doc) {
213
+ try {
214
+ const payload = {
215
+ apiKey: generatedApiKey,
216
+ siteName: desiredSiteName,
217
+ siteLogo: desiredSiteLogoArr,
218
+ };
219
+ const created = await settingsModel.create(payload);
220
+ // ensure we keep in-memory api key
221
+ this.apiKey = payload.apiKey;
222
+ Logger_1.default.info('DefaultDataInitializer', 'Settings document created.');
223
+ return;
224
+ }
225
+ catch (err) {
226
+ Logger_1.default.error('DefaultDataInitializer', 'Failed to create Settings document:', err);
227
+ // still store generated in memory for server use
228
+ this.apiKey = generatedApiKey;
229
+ return;
230
+ }
231
+ }
232
+ // Update only missing/empty fields (do not overwrite existing values)
233
+ const updates = {};
234
+ // apiKey
235
+ if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
236
+ this.apiKey = doc.apiKey; // honor existing
237
+ }
238
+ else {
239
+ updates.apiKey = generatedApiKey;
240
+ this.apiKey = generatedApiKey;
241
+ }
242
+ // siteName
243
+ if (!doc.siteName || String(doc.siteName).trim().length === 0) {
244
+ updates.siteName = desiredSiteName;
245
+ }
246
+ // siteLogo (array of strings). If absent or empty, seed with default SVG.
247
+ const siteLogoArray = doc.siteLogo;
248
+ const hasValidLogo = Array.isArray(siteLogoArray) &&
249
+ siteLogoArray.length > 0 &&
250
+ typeof siteLogoArray[0] === 'string' &&
251
+ String(siteLogoArray[0]).trim().length > 0;
252
+ if (!hasValidLogo) {
253
+ updates.siteLogo = desiredSiteLogoArr;
254
+ }
255
+ if (Object.keys(updates).length) {
256
+ try {
257
+ await settingsModel.update(doc.id ?? doc._id, updates);
258
+ Logger_1.default.info('DefaultDataInitializer', 'Settings document normalized/updated.');
259
+ }
260
+ catch (err) {
261
+ Logger_1.default.error('DefaultDataInitializer', 'Failed to update Settings document:', err);
262
+ }
263
+ }
264
+ else {
265
+ Logger_1.default.info('DefaultDataInitializer', 'Settings document already OK.');
266
+ }
267
+ }
268
+ }
269
+ exports.DefaultDataInitializer = DefaultDataInitializer;
@@ -0,0 +1,12 @@
1
+ export declare class Logger {
2
+ private isDevelopment;
3
+ constructor();
4
+ private getCallerInfo;
5
+ private formatMessage;
6
+ log(label: string, ...messages: any[]): void;
7
+ info(label: string, ...messages: any[]): void;
8
+ warn(label: string, ...messages: any[]): void;
9
+ error(label: string, ...messages: any[]): void;
10
+ }
11
+ declare const logger: Logger;
12
+ export default logger;
@@ -0,0 +1,79 @@
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.Logger = void 0;
7
+ const kleur_1 = __importDefault(require("kleur"));
8
+ const util_1 = require("util");
9
+ const path_1 = __importDefault(require("path"));
10
+ class Logger {
11
+ constructor() {
12
+ const env = process.env.NODE_ENV || 'development';
13
+ this.isDevelopment = env === 'development';
14
+ }
15
+ getCallerInfo() {
16
+ const originalPrepareStackTrace = Error.prepareStackTrace;
17
+ Error.prepareStackTrace = (err, stack) => stack;
18
+ const err = new Error();
19
+ Error.captureStackTrace(err, this.getCallerInfo);
20
+ const stack = err.stack;
21
+ Error.prepareStackTrace = originalPrepareStackTrace;
22
+ // Find the first stack frame outside of Logger class and this function
23
+ const callerFrame = stack.find((frame) => frame.getFileName() &&
24
+ !frame.getFileName()?.includes('Logger.ts') &&
25
+ !frame.getFunctionName()?.includes('getCallerInfo'));
26
+ if (!callerFrame) {
27
+ return 'unknown:0:0';
28
+ }
29
+ const fileName = path_1.default.relative(process.cwd(), callerFrame.getFileName() || 'unknown');
30
+ const lineNumber = callerFrame.getLineNumber() || 0;
31
+ const columnNumber = callerFrame.getColumnNumber() || 0;
32
+ return `${fileName}:${lineNumber}:${columnNumber}`;
33
+ }
34
+ formatMessage(level, label, messages) {
35
+ const callerInfo = this.getCallerInfo();
36
+ let formattedLabel = '';
37
+ switch (level) {
38
+ case 'LOG':
39
+ formattedLabel = kleur_1.default.black(`[LOG] ${label} (${callerInfo}):`);
40
+ break;
41
+ case 'INFO':
42
+ formattedLabel = kleur_1.default.cyan(`[INFO] ${label} (${callerInfo}):`);
43
+ break;
44
+ case 'WARN':
45
+ formattedLabel = kleur_1.default.yellow(`[WARN] ${label} (${callerInfo}):`);
46
+ break;
47
+ case 'ERROR':
48
+ formattedLabel = kleur_1.default.red(`[ERROR] ${label} (${callerInfo}):`);
49
+ break;
50
+ default:
51
+ formattedLabel = `[${level}] ${label} (${callerInfo}):`;
52
+ }
53
+ const formattedMessage = (0, util_1.format)(...messages);
54
+ return `${formattedLabel} ${formattedMessage}`;
55
+ }
56
+ log(label, ...messages) {
57
+ if (this.isDevelopment) {
58
+ const formatted = this.formatMessage('LOG', label, messages);
59
+ console.log(formatted);
60
+ }
61
+ }
62
+ info(label, ...messages) {
63
+ if (this.isDevelopment) {
64
+ const formatted = this.formatMessage('INFO', label, messages);
65
+ console.info(formatted);
66
+ }
67
+ }
68
+ warn(label, ...messages) {
69
+ const formatted = this.formatMessage('WARN', label, messages);
70
+ console.warn(formatted);
71
+ }
72
+ error(label, ...messages) {
73
+ const formatted = this.formatMessage('ERROR', label, messages);
74
+ console.error(formatted);
75
+ }
76
+ }
77
+ exports.Logger = Logger;
78
+ const logger = new Logger();
79
+ exports.default = logger;
@@ -0,0 +1,51 @@
1
+ import { Schema } from '../models/BaseModel';
2
+ import { FieldIndexSpec } from '../database/DatabaseAdapter';
3
+ type PublicAttributes = Record<string, any>;
4
+ type PublicSchema = Omit<Schema, 'attributes'> & {
5
+ modelName: string;
6
+ attributes: PublicAttributes;
7
+ allowedMethods: Schema['allowedMethods'];
8
+ };
9
+ export declare class SchemaLoader {
10
+ private emitter;
11
+ schemas: {
12
+ [key: string]: Schema;
13
+ };
14
+ private isDevelopment;
15
+ private nonOverridableSchemas;
16
+ private userSchemasDir;
17
+ private packageSchemasDir;
18
+ private watcher;
19
+ private static _instance;
20
+ static getInstance(): SchemaLoader;
21
+ constructor();
22
+ private setupHotReload;
23
+ private handleFileChange;
24
+ on(event: 'schemasChanged', listener: (schemas: {
25
+ [key: string]: Schema;
26
+ }) => void): void;
27
+ private loadSchemas;
28
+ private processSchemas;
29
+ private loadSchemasFromDirectory;
30
+ private mergeAttributes;
31
+ getSchemas(): {
32
+ [key: string]: Schema;
33
+ };
34
+ /** CLIENT/API: sanitized map with private attributes removed */
35
+ getPublicSchemas(): Record<string, PublicSchema>;
36
+ /** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
37
+ getPublicSchemaList(): Array<PublicSchema>;
38
+ /** Strip any attr marked private; also remove the `private` flag from others */
39
+ /** Keep private/sensitive flags so the UI and policy layer can decide.
40
+ * We only shallow-clone values to avoid leaking references.
41
+ */
42
+ private sanitizeAttributes;
43
+ closeWatcher(): void;
44
+ private coerceIndexDir;
45
+ /** Build desired single-field indexes per model from schemas.
46
+ * - respects attribute.index (true|1|-1|'asc'|'desc')
47
+ * - always adds createdAt/updatedAt (desc) to help common sorts.
48
+ */
49
+ getIndexPlan(): Record<string, FieldIndexSpec>;
50
+ }
51
+ export {};