lumisjs 1.0.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.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +160 -0
- package/index.js +90 -0
- package/package.json +68 -0
- package/src/cache/CacheManager.js +393 -0
- package/src/cache/MemoryAdapter.js +259 -0
- package/src/cache/MultiLevelCache.js +362 -0
- package/src/cache/RedisAdapter.js +329 -0
- package/src/cache/SQLiteAdapter.js +280 -0
- package/src/cache/index.js +15 -0
- package/src/client/Client.js +554 -0
- package/src/client/ShardingManager.js +274 -0
- package/src/config/ConfigValidator.js +172 -0
- package/src/config/index.js +7 -0
- package/src/dashboard/DashboardManager.js +362 -0
- package/src/datagen/DataGenerator.js +47 -0
- package/src/datagen/apis/GraphqlMocker.js +16 -0
- package/src/datagen/apis/OpenApiMocker.js +18 -0
- package/src/datagen/cli.js +130 -0
- package/src/datagen/formatters/csv.js +45 -0
- package/src/datagen/formatters/index.js +15 -0
- package/src/datagen/formatters/json.js +33 -0
- package/src/datagen/formatters/mongo.js +22 -0
- package/src/datagen/formatters/sql.js +32 -0
- package/src/datagen/index.js +17 -0
- package/src/datagen/plugin/PluginManager.js +43 -0
- package/src/datagen/plugins/example-plugin.js +21 -0
- package/src/datagen/schema/SchemaGenerator.js +172 -0
- package/src/datagen/schema/loadSchema.js +14 -0
- package/src/datagen/utils/seed.js +26 -0
- package/src/di/ServiceContainer.js +229 -0
- package/src/errors/ErrorCodes.js +110 -0
- package/src/errors/LumisError.js +85 -0
- package/src/game/EconomyManager.js +161 -0
- package/src/game/GameSessionManager.js +76 -0
- package/src/game/GuildManager.js +197 -0
- package/src/game/InventoryManager.js +140 -0
- package/src/game/LevelingSystem.js +194 -0
- package/src/game/MusicManager.js +587 -0
- package/src/health/HealthChecker.js +170 -0
- package/src/health/index.js +7 -0
- package/src/performance/PerformanceMonitor.js +244 -0
- package/src/performance/index.js +7 -0
- package/src/security/InputSanitizer.js +185 -0
- package/src/security/SecurityMiddleware.js +231 -0
- package/src/security/index.js +9 -0
- package/src/shutdown/GracefulShutdown.js +159 -0
- package/src/shutdown/index.js +7 -0
- package/src/utils/StructuredLogger.js +118 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { URL } = require('url');
|
|
5
|
+
const helmet = require('helmet');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Built-in Dashboard & Analytics API.
|
|
9
|
+
* Provides an HTTP REST API + real-time metrics for monitoring your bot.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const client = new Client({
|
|
13
|
+
* dashboard: { enabled: true, port: 3000, auth: 'my-secret-key' }
|
|
14
|
+
* });
|
|
15
|
+
* // GET /api/stats
|
|
16
|
+
* // GET /api/guilds
|
|
17
|
+
* // GET /api/commands
|
|
18
|
+
* // GET /api/health
|
|
19
|
+
* // GET /api/cache
|
|
20
|
+
* // GET /api/plugins
|
|
21
|
+
* // GET /api/services
|
|
22
|
+
* // GET /api/middleware
|
|
23
|
+
*/
|
|
24
|
+
class DashboardManager {
|
|
25
|
+
constructor(client, options = {}) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
this.options = {
|
|
28
|
+
enabled: options.enabled || false,
|
|
29
|
+
port: options.port || 3000,
|
|
30
|
+
host: options.host || '0.0.0.0',
|
|
31
|
+
auth: options.auth || null,
|
|
32
|
+
cors: options.cors !== false,
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** @type {http.Server|null} */
|
|
37
|
+
this._server = null;
|
|
38
|
+
|
|
39
|
+
/** @type {Map<string, number>} Command usage counters */
|
|
40
|
+
this._commandUsage = new Map();
|
|
41
|
+
|
|
42
|
+
/** @type {Array} Recent errors */
|
|
43
|
+
this._recentErrors = [];
|
|
44
|
+
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
this._requestCount = 0;
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, Function>} Custom API routes */
|
|
49
|
+
this._customRoutes = new Map();
|
|
50
|
+
|
|
51
|
+
// Track command usage
|
|
52
|
+
this._trackUsage();
|
|
53
|
+
|
|
54
|
+
// Auto-start if enabled
|
|
55
|
+
if (this.options.enabled) {
|
|
56
|
+
this.client.on?.('ready', () => this.start());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start the dashboard HTTP server.
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async start() {
|
|
65
|
+
if (this._server) return;
|
|
66
|
+
|
|
67
|
+
this._server = http.createServer((req, res) => {
|
|
68
|
+
this._handleRequest(req, res);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
this._server.listen(this.options.port, this.options.host, () => {
|
|
73
|
+
this.client.logger?.info(`📊 Dashboard running at http://localhost:${this.options.port}`);
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this._server.on('error', (err) => {
|
|
78
|
+
this.client.logger?.error('Dashboard server error:', err.message);
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stop the dashboard server.
|
|
86
|
+
*/
|
|
87
|
+
async stop() {
|
|
88
|
+
if (!this._server) return;
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
this._server.close(() => {
|
|
91
|
+
this._server = null;
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a custom API route.
|
|
99
|
+
* @param {string} path - e.g. '/api/custom'
|
|
100
|
+
* @param {Function} handler - (req, res, client) => void
|
|
101
|
+
*/
|
|
102
|
+
route(path, handler) {
|
|
103
|
+
this._customRoutes.set(path, handler);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @private */
|
|
108
|
+
_handleRequest(req, res) {
|
|
109
|
+
this._requestCount++;
|
|
110
|
+
|
|
111
|
+
// Apply Helmet security headers
|
|
112
|
+
helmet({
|
|
113
|
+
contentSecurityPolicy: {
|
|
114
|
+
directives: {
|
|
115
|
+
defaultSrc: ["'self'"],
|
|
116
|
+
scriptSrc: ["'self'"],
|
|
117
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
118
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
})(req, res, () => {});
|
|
122
|
+
|
|
123
|
+
// CORS
|
|
124
|
+
if (this.options.cors) {
|
|
125
|
+
res.setHeader('Access-Control-Allow-Origin', this.options.corsOrigin || '*');
|
|
126
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
127
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
128
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
129
|
+
if (req.method === 'OPTIONS') {
|
|
130
|
+
res.writeHead(204);
|
|
131
|
+
res.end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Auth
|
|
137
|
+
if (this.options.auth) {
|
|
138
|
+
const authHeader = req.headers.authorization;
|
|
139
|
+
if (!authHeader || authHeader !== `Bearer ${this.options.auth}`) {
|
|
140
|
+
this._json(res, 401, { error: 'Unauthorized' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
146
|
+
const pathname = url.pathname;
|
|
147
|
+
|
|
148
|
+
// Custom routes first
|
|
149
|
+
if (this._customRoutes.has(pathname)) {
|
|
150
|
+
try {
|
|
151
|
+
this._customRoutes.get(pathname)(req, res, this.client);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this._json(res, 500, { error: err.message });
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Built-in routes
|
|
159
|
+
switch (pathname) {
|
|
160
|
+
case '/api/stats':
|
|
161
|
+
return this._routeStats(res);
|
|
162
|
+
case '/api/guilds':
|
|
163
|
+
return this._routeGuilds(res);
|
|
164
|
+
case '/api/commands':
|
|
165
|
+
return this._routeCommands(res);
|
|
166
|
+
case '/api/health':
|
|
167
|
+
return this._routeHealth(res);
|
|
168
|
+
case '/api/cache':
|
|
169
|
+
return this._routeCache(res);
|
|
170
|
+
case '/api/plugins':
|
|
171
|
+
return this._routePlugins(res);
|
|
172
|
+
case '/api/services':
|
|
173
|
+
return this._routeServices(res);
|
|
174
|
+
case '/api/middleware':
|
|
175
|
+
return this._routeMiddleware(res);
|
|
176
|
+
case '/api/errors':
|
|
177
|
+
return this._routeErrors(res);
|
|
178
|
+
default:
|
|
179
|
+
this._json(res, 404, { error: 'Not Found', availableRoutes: [
|
|
180
|
+
'/api/stats', '/api/guilds', '/api/commands', '/api/health',
|
|
181
|
+
'/api/cache', '/api/plugins', '/api/services', '/api/middleware',
|
|
182
|
+
'/api/errors',
|
|
183
|
+
]});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @private */
|
|
188
|
+
_routeStats(res) {
|
|
189
|
+
const memUsage = process.memoryUsage();
|
|
190
|
+
|
|
191
|
+
this._json(res, 200, {
|
|
192
|
+
bot: {
|
|
193
|
+
user: this.client.user ? {
|
|
194
|
+
id: this.client.user.id,
|
|
195
|
+
tag: this.client.user.tag,
|
|
196
|
+
avatar: this.client.user.avatar,
|
|
197
|
+
} : null,
|
|
198
|
+
uptime: this.client.uptime,
|
|
199
|
+
uptimeFormatted: this._formatUptime(this.client.uptime),
|
|
200
|
+
ping: this.client.ping,
|
|
201
|
+
ready: this.client.ready,
|
|
202
|
+
readyAt: this.client.readyAt,
|
|
203
|
+
},
|
|
204
|
+
counts: {
|
|
205
|
+
guilds: this.client.guilds?.size || 0,
|
|
206
|
+
channels: this.client.channels?.size || 0,
|
|
207
|
+
users: this.client.users?.size || 0,
|
|
208
|
+
commands: this.client.commands?.commands?.size || 0,
|
|
209
|
+
},
|
|
210
|
+
memory: {
|
|
211
|
+
rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB',
|
|
212
|
+
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
|
|
213
|
+
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB',
|
|
214
|
+
},
|
|
215
|
+
system: {
|
|
216
|
+
nodeVersion: process.version,
|
|
217
|
+
platform: process.platform,
|
|
218
|
+
pid: process.pid,
|
|
219
|
+
},
|
|
220
|
+
shard: this.client.shard || null,
|
|
221
|
+
dashboard: {
|
|
222
|
+
requests: this._requestCount,
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @private */
|
|
228
|
+
_routeGuilds(res) {
|
|
229
|
+
const guilds = [];
|
|
230
|
+
if (this.client.guilds) {
|
|
231
|
+
for (const [id, guild] of this.client.guilds) {
|
|
232
|
+
guilds.push({
|
|
233
|
+
id,
|
|
234
|
+
name: guild.name,
|
|
235
|
+
memberCount: guild.memberCount,
|
|
236
|
+
icon: guild.icon,
|
|
237
|
+
ownerId: guild.ownerId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this._json(res, 200, { total: guilds.length, guilds });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** @private */
|
|
245
|
+
_routeCommands(res) {
|
|
246
|
+
const commands = [];
|
|
247
|
+
if (this.client.commands?.commands) {
|
|
248
|
+
for (const [name, cmd] of this.client.commands.commands) {
|
|
249
|
+
commands.push({
|
|
250
|
+
name,
|
|
251
|
+
description: cmd.description || '',
|
|
252
|
+
cooldown: cmd.cooldown || 0,
|
|
253
|
+
guards: cmd.guards?.map(g => g.name) || [],
|
|
254
|
+
usage: this._commandUsage.get(name) || 0,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
this._json(res, 200, { total: commands.length, commands });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** @private */
|
|
262
|
+
_routeHealth(res) {
|
|
263
|
+
const healthy = this.client.ready && this.client.ws?.connected !== false;
|
|
264
|
+
this._json(res, healthy ? 200 : 503, {
|
|
265
|
+
status: healthy ? 'healthy' : 'unhealthy',
|
|
266
|
+
ready: this.client.ready,
|
|
267
|
+
uptime: this.client.uptime,
|
|
268
|
+
ping: this.client.ping,
|
|
269
|
+
timestamp: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** @private */
|
|
274
|
+
_routeCache(res) {
|
|
275
|
+
this._json(res, 200, {
|
|
276
|
+
guilds: this.client.guilds?.size || 0,
|
|
277
|
+
channels: this.client.channels?.size || 0,
|
|
278
|
+
users: this.client.users?.size || 0,
|
|
279
|
+
adapter: this.client.cache?.adapter?.constructor?.name || 'unknown',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** @private */
|
|
284
|
+
_routePlugins(res) {
|
|
285
|
+
const plugins = this.client.plugins?.list?.() || [];
|
|
286
|
+
this._json(res, 200, { total: plugins.length, plugins });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @private */
|
|
290
|
+
_routeServices(res) {
|
|
291
|
+
const services = this.client.services?.list?.() || [];
|
|
292
|
+
this._json(res, 200, { total: services.length, services });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @private */
|
|
296
|
+
_routeMiddleware(res) {
|
|
297
|
+
const stats = this.client.middleware?.stats || { global: 0, command: 0, group: 0, total: 0 };
|
|
298
|
+
this._json(res, 200, stats);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @private */
|
|
302
|
+
_routeErrors(res) {
|
|
303
|
+
this._json(res, 200, {
|
|
304
|
+
total: this._recentErrors.length,
|
|
305
|
+
errors: this._recentErrors.slice(-50)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @private */
|
|
310
|
+
_trackUsage() {
|
|
311
|
+
this.client.on?.('interactionCreate', (interaction) => {
|
|
312
|
+
if (interaction?.commandName) {
|
|
313
|
+
const current = this._commandUsage.get(interaction.commandName) || 0;
|
|
314
|
+
this._commandUsage.set(interaction.commandName, current + 1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
this.client.on?.('error', (error) => {
|
|
319
|
+
this._recentErrors.push({
|
|
320
|
+
message: error?.message || String(error),
|
|
321
|
+
timestamp: new Date().toISOString(),
|
|
322
|
+
});
|
|
323
|
+
if (this._recentErrors.length > 100) {
|
|
324
|
+
this._recentErrors = this._recentErrors.slice(-100);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** @private */
|
|
330
|
+
_json(res, status, data) {
|
|
331
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
332
|
+
res.end(JSON.stringify(data, null, 2));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** @private */
|
|
336
|
+
_formatUptime(ms) {
|
|
337
|
+
if (!ms) return '0s';
|
|
338
|
+
const seconds = Math.floor(ms / 1000);
|
|
339
|
+
const minutes = Math.floor(seconds / 60);
|
|
340
|
+
const hours = Math.floor(minutes / 60);
|
|
341
|
+
const days = Math.floor(hours / 24);
|
|
342
|
+
|
|
343
|
+
const parts = [];
|
|
344
|
+
if (days > 0) parts.push(`${days}d`);
|
|
345
|
+
if (hours % 24 > 0) parts.push(`${hours % 24}h`);
|
|
346
|
+
if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
|
|
347
|
+
if (seconds % 60 > 0) parts.push(`${seconds % 60}s`);
|
|
348
|
+
return parts.join(' ') || '0s';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Destroy the dashboard.
|
|
353
|
+
*/
|
|
354
|
+
async destroy() {
|
|
355
|
+
await this.stop();
|
|
356
|
+
this._commandUsage.clear();
|
|
357
|
+
this._recentErrors = [];
|
|
358
|
+
this._customRoutes.clear();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = DashboardManager;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SchemaGenerator } = require('./schema/SchemaGenerator');
|
|
4
|
+
|
|
5
|
+
class DataGenerator {
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* @param {object} options.schema - JSON Schema definition
|
|
9
|
+
* @param {string|number} [options.seed] - Optional seed for reproducible output
|
|
10
|
+
* @param {string} [options.locale='en'] - Locale hint for faker
|
|
11
|
+
* @param {object} [options.pluginManager] - Optional plugin manager
|
|
12
|
+
* @param {number} [options.count=1] - Default number of records to generate
|
|
13
|
+
*/
|
|
14
|
+
constructor({ schema, seed, locale = 'en', pluginManager, count = 1 } = {}) {
|
|
15
|
+
if (!schema) throw new TypeError('schema is required');
|
|
16
|
+
this.schema = schema;
|
|
17
|
+
this.seed = seed;
|
|
18
|
+
this.locale = locale;
|
|
19
|
+
this.pluginManager = pluginManager;
|
|
20
|
+
this.count = count;
|
|
21
|
+
|
|
22
|
+
this._generator = new SchemaGenerator({ schema, seed, locale, pluginManager });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a single record.
|
|
27
|
+
* @returns {any}
|
|
28
|
+
*/
|
|
29
|
+
generate() {
|
|
30
|
+
return this._generator.generate();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate multiple records.
|
|
35
|
+
* @param {number} [count] - Overrides instance count if provided
|
|
36
|
+
* @returns {any[]}
|
|
37
|
+
*/
|
|
38
|
+
generateMany(count) {
|
|
39
|
+
const n = count ?? this.count;
|
|
40
|
+
if (typeof n !== 'number' || n < 1 || !Number.isInteger(n)) {
|
|
41
|
+
throw new RangeError('count must be a positive integer');
|
|
42
|
+
}
|
|
43
|
+
return Array.from({ length: n }, () => this._generator.generate());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { DataGenerator };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal GraphQL mock API helper.
|
|
5
|
+
*
|
|
6
|
+
* TODO: hook into graphql-js and generate resolvers based on schema definitions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function mockFromGraphqlSchema(schema, options = {}) {
|
|
10
|
+
return {
|
|
11
|
+
schema,
|
|
12
|
+
mocks: {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { mockFromGraphqlSchema };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal OpenAPI mocker helper.
|
|
5
|
+
*
|
|
6
|
+
* TODO: implement full OpenAPI parsing + mock data generation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function mockFromOpenApi(openApiSpec, options = {}) {
|
|
10
|
+
// Placeholder: Walk through the OpenAPI paths and generate responses for each operation.
|
|
11
|
+
// Real implementation should support request/response schemas, security, and status codes.
|
|
12
|
+
return {
|
|
13
|
+
info: openApiSpec.info,
|
|
14
|
+
paths: {},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { mockFromOpenApi };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { DataGenerator } = require('./DataGenerator');
|
|
6
|
+
const { loadSchema } = require('./schema/loadSchema');
|
|
7
|
+
const { formatters } = require('./formatters');
|
|
8
|
+
const { PluginManager } = require('./plugin/PluginManager');
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const args = {};
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < argv.length) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg.startsWith('--')) {
|
|
16
|
+
const key = arg.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (next && !next.startsWith('--')) {
|
|
19
|
+
args[key] = next;
|
|
20
|
+
i += 2;
|
|
21
|
+
} else {
|
|
22
|
+
args[key] = true;
|
|
23
|
+
i += 1;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
// positional
|
|
27
|
+
if (!args._) args._ = [];
|
|
28
|
+
args._.push(arg);
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showHelp() {
|
|
36
|
+
console.log(`
|
|
37
|
+
lumis-datagen — schema-based mock data generator
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
lumis-datagen --schema schema.json --count 1000 --out out.json [options]
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--schema <file> JSON schema file (required)
|
|
44
|
+
--count <n> Number of records to generate (default: 1)
|
|
45
|
+
--out <file> Output file path (default: stdout)
|
|
46
|
+
--format <fmt> Output format: json|csv|sql|mongo (default: json)
|
|
47
|
+
--seed <value> Deterministic seed for repeatable data
|
|
48
|
+
--locale <locale> Locale for locale-aware generators (default: en)
|
|
49
|
+
--plugin-dir <dir> Path to load plugins from
|
|
50
|
+
--batch <size> Batch size for streaming output
|
|
51
|
+
--help Show this help message
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureExists(filePath) {
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
throw new Error(`File not found: ${filePath}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runCli({ argv, cwd, rootDir }) {
|
|
62
|
+
const opts = parseArgs(argv);
|
|
63
|
+
if (opts.help || opts.h) {
|
|
64
|
+
showHelp();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const schemaPath = opts.schema || opts.s;
|
|
69
|
+
if (!schemaPath) {
|
|
70
|
+
console.error('Error: --schema is required');
|
|
71
|
+
showHelp();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const schemaFile = path.isAbsolute(schemaPath) ? schemaPath : path.join(cwd, schemaPath);
|
|
76
|
+
ensureExists(schemaFile);
|
|
77
|
+
|
|
78
|
+
const count = Number(opts.count || opts.c || 1);
|
|
79
|
+
const outPath = opts.out || opts.o;
|
|
80
|
+
const format = (opts.format || 'json').toLowerCase();
|
|
81
|
+
const seed = opts.seed || opts._seed || undefined;
|
|
82
|
+
const locale = opts.locale || 'en';
|
|
83
|
+
const pluginDir = opts['plugin-dir'] || opts.p || null;
|
|
84
|
+
const batchSize = Number(opts.batch || 0);
|
|
85
|
+
|
|
86
|
+
const schema = loadSchema(schemaFile);
|
|
87
|
+
const pluginManager = new PluginManager({ pluginDir, rootDir, locale });
|
|
88
|
+
|
|
89
|
+
const generator = new DataGenerator({ schema, seed, locale, pluginManager });
|
|
90
|
+
const outputFormatter = formatters[format];
|
|
91
|
+
if (!outputFormatter) {
|
|
92
|
+
console.error(`Unknown format: ${format}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const stream = outputFormatter(streamGenerator(generator, count, batchSize), { schema, count, locale });
|
|
97
|
+
|
|
98
|
+
if (outPath) {
|
|
99
|
+
const outFile = path.isAbsolute(outPath) ? outPath : path.join(cwd, outPath);
|
|
100
|
+
const writeStream = fs.createWriteStream(outFile, { encoding: 'utf8' });
|
|
101
|
+
stream.pipe(writeStream);
|
|
102
|
+
writeStream.on('finish', () => {
|
|
103
|
+
console.log(`✅ Generated ${count} record(s) to ${outFile}`);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
stream.pipe(process.stdout);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function* streamGenerator(generator, count, batchSize) {
|
|
111
|
+
if (!batchSize || batchSize < 1) {
|
|
112
|
+
for (let i = 0; i < count; i += 1) {
|
|
113
|
+
yield generator.generate();
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let remaining = count;
|
|
119
|
+
while (remaining > 0) {
|
|
120
|
+
const currentBatch = Math.min(batchSize, remaining);
|
|
121
|
+
const records = [];
|
|
122
|
+
for (let i = 0; i < currentBatch; i += 1) {
|
|
123
|
+
records.push(generator.generate());
|
|
124
|
+
}
|
|
125
|
+
yield records;
|
|
126
|
+
remaining -= currentBatch;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { runCli };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Readable } = require('stream');
|
|
4
|
+
|
|
5
|
+
function CsvFormatter(generator, { schema }) {
|
|
6
|
+
const stream = new Readable({ objectMode: true, read() {} });
|
|
7
|
+
const fields = Object.keys(schema.properties || {});
|
|
8
|
+
let headerWritten = false;
|
|
9
|
+
|
|
10
|
+
const toCsv = (record) => {
|
|
11
|
+
return fields
|
|
12
|
+
.map((key) => {
|
|
13
|
+
let value = record[key];
|
|
14
|
+
if (value === undefined || value === null) return '';
|
|
15
|
+
const str = String(value);
|
|
16
|
+
// escape "
|
|
17
|
+
const escaped = str.replace(/"/g, '""');
|
|
18
|
+
if (escaped.includes(',') || escaped.includes('"') || escaped.includes('\n')) {
|
|
19
|
+
return `"${escaped}"`;
|
|
20
|
+
}
|
|
21
|
+
return escaped;
|
|
22
|
+
})
|
|
23
|
+
.join(',');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
stream._read = function () {
|
|
27
|
+
if (!headerWritten) {
|
|
28
|
+
this.push(fields.join(',') + '\n');
|
|
29
|
+
headerWritten = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { value, done } = generator.next ? generator.next() : { done: true };
|
|
34
|
+
if (done) {
|
|
35
|
+
this.push(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.push(toCsv(value) + '\n');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return stream;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { CsvFormatter };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { JsonFormatter } = require('./json');
|
|
4
|
+
const { CsvFormatter } = require('./csv');
|
|
5
|
+
const { SqlFormatter } = require('./sql');
|
|
6
|
+
const { MongoFormatter } = require('./mongo');
|
|
7
|
+
|
|
8
|
+
const formatters = {
|
|
9
|
+
json: JsonFormatter,
|
|
10
|
+
csv: CsvFormatter,
|
|
11
|
+
sql: SqlFormatter,
|
|
12
|
+
mongo: MongoFormatter,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = { formatters };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Readable } = require('stream');
|
|
4
|
+
|
|
5
|
+
function JsonFormatter(generator, { count }) {
|
|
6
|
+
const stream = new Readable({ objectMode: true, read() {} });
|
|
7
|
+
let wroteHeader = false;
|
|
8
|
+
stream._read = function () {
|
|
9
|
+
if (!wroteHeader) {
|
|
10
|
+
this.push('[');
|
|
11
|
+
wroteHeader = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { value, done } = generator.next ? generator.next() : { done: true };
|
|
15
|
+
if (done) {
|
|
16
|
+
this.push(']');
|
|
17
|
+
this.push(null);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const json = JSON.stringify(value, null, 2);
|
|
22
|
+
if (this._firstRecord) {
|
|
23
|
+
this._firstRecord = false;
|
|
24
|
+
this.push(`\n${json}`);
|
|
25
|
+
} else {
|
|
26
|
+
this.push(`,\n${json}`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
stream._firstRecord = true;
|
|
30
|
+
return stream;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { JsonFormatter };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Readable } = require('stream');
|
|
4
|
+
|
|
5
|
+
function MongoFormatter(generator, { schema }) {
|
|
6
|
+
const collection = schema.collection || 'data';
|
|
7
|
+
const stream = new Readable({ objectMode: true, read() {} });
|
|
8
|
+
|
|
9
|
+
stream._read = function () {
|
|
10
|
+
const { value, done } = generator.next ? generator.next() : { done: true };
|
|
11
|
+
if (done) {
|
|
12
|
+
this.push(null);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.push(`db.getCollection('${collection}').insertOne(${JSON.stringify(value, null, 2)});\n`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return stream;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { MongoFormatter };
|