@almadar/server 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/dist/index.js ADDED
@@ -0,0 +1,1015 @@
1
+ import { z, ZodError } from 'zod';
2
+ import dotenv from 'dotenv';
3
+ import admin from 'firebase-admin';
4
+ export { default as admin } from 'firebase-admin';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ import { faker } from '@faker-js/faker';
7
+
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ dotenv.config();
15
+ var envSchema = z.object({
16
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
17
+ PORT: z.string().default("3030").transform((val) => parseInt(val, 10)),
18
+ CORS_ORIGIN: z.string().default("http://localhost:5173").transform((val) => val.includes(",") ? val.split(",").map((s) => s.trim()) : val),
19
+ // Database (Prisma/SQL) - optional
20
+ DATABASE_URL: z.string().optional(),
21
+ // Firebase/Firestore configuration
22
+ FIREBASE_PROJECT_ID: z.string().optional(),
23
+ FIREBASE_CLIENT_EMAIL: z.string().optional(),
24
+ FIREBASE_PRIVATE_KEY: z.string().optional(),
25
+ FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),
26
+ FIRESTORE_EMULATOR_HOST: z.string().optional(),
27
+ FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
28
+ // API configuration
29
+ API_PREFIX: z.string().default("/api"),
30
+ // Mock data configuration
31
+ USE_MOCK_DATA: z.string().default("true").transform((v) => v === "true"),
32
+ MOCK_SEED: z.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
33
+ });
34
+ var parsed = envSchema.safeParse(process.env);
35
+ if (!parsed.success) {
36
+ console.error("\u274C Invalid environment variables:", parsed.error.flatten().fieldErrors);
37
+ throw new Error("Invalid environment variables");
38
+ }
39
+ var env = parsed.data;
40
+
41
+ // src/lib/logger.ts
42
+ var colors = {
43
+ debug: "\x1B[36m",
44
+ // Cyan
45
+ info: "\x1B[32m",
46
+ // Green
47
+ warn: "\x1B[33m",
48
+ // Yellow
49
+ error: "\x1B[31m",
50
+ // Red
51
+ reset: "\x1B[0m"
52
+ };
53
+ var shouldLog = (level) => {
54
+ const levels = ["debug", "info", "warn", "error"];
55
+ const minLevel = env.NODE_ENV === "production" ? "info" : "debug";
56
+ return levels.indexOf(level) >= levels.indexOf(minLevel);
57
+ };
58
+ var formatMessage = (level, message, meta) => {
59
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
60
+ const color = colors[level];
61
+ const prefix = `${color}[${level.toUpperCase()}]${colors.reset}`;
62
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
63
+ return `${timestamp} ${prefix} ${message}${metaStr}`;
64
+ };
65
+ var logger = {
66
+ debug: (message, meta) => {
67
+ if (shouldLog("debug")) {
68
+ console.log(formatMessage("debug", message, meta));
69
+ }
70
+ },
71
+ info: (message, meta) => {
72
+ if (shouldLog("info")) {
73
+ console.log(formatMessage("info", message, meta));
74
+ }
75
+ },
76
+ warn: (message, meta) => {
77
+ if (shouldLog("warn")) {
78
+ console.warn(formatMessage("warn", message, meta));
79
+ }
80
+ },
81
+ error: (message, meta) => {
82
+ if (shouldLog("error")) {
83
+ console.error(formatMessage("error", message, meta));
84
+ }
85
+ }
86
+ };
87
+
88
+ // src/lib/eventBus.ts
89
+ var EventBus = class {
90
+ handlers = /* @__PURE__ */ new Map();
91
+ debug;
92
+ constructor(options) {
93
+ this.debug = options?.debug ?? false;
94
+ }
95
+ on(event, handler) {
96
+ if (!this.handlers.has(event)) {
97
+ this.handlers.set(event, /* @__PURE__ */ new Set());
98
+ }
99
+ this.handlers.get(event).add(handler);
100
+ return () => {
101
+ this.handlers.get(event)?.delete(handler);
102
+ };
103
+ }
104
+ off(event, handler) {
105
+ this.handlers.get(event)?.delete(handler);
106
+ }
107
+ emit(event, payload, meta) {
108
+ if (this.debug) {
109
+ console.log(`[EventBus] Emitting ${event}:`, payload);
110
+ }
111
+ const handlers = this.handlers.get(event);
112
+ if (handlers) {
113
+ handlers.forEach((handler) => {
114
+ try {
115
+ handler(payload, meta);
116
+ } catch (err) {
117
+ console.error(`[EventBus] Error in handler for ${event}:`, err);
118
+ }
119
+ });
120
+ }
121
+ }
122
+ clear() {
123
+ this.handlers.clear();
124
+ }
125
+ };
126
+ var serverEventBus = new EventBus({
127
+ debug: process.env.NODE_ENV === "development"
128
+ });
129
+ function emitEntityEvent(entityType, action, payload) {
130
+ const eventType = `${entityType.toUpperCase()}_${action}`;
131
+ serverEventBus.emit(eventType, payload, { orbital: entityType });
132
+ }
133
+ var firebaseApp = null;
134
+ function initializeFirebase() {
135
+ if (firebaseApp) {
136
+ return firebaseApp;
137
+ }
138
+ if (admin.apps.length > 0) {
139
+ firebaseApp = admin.apps[0];
140
+ return firebaseApp;
141
+ }
142
+ if (env.FIRESTORE_EMULATOR_HOST) {
143
+ firebaseApp = admin.initializeApp({
144
+ projectId: env.FIREBASE_PROJECT_ID || "demo-project"
145
+ });
146
+ console.log(`\u{1F527} Firebase Admin initialized for emulator: ${env.FIRESTORE_EMULATOR_HOST}`);
147
+ return firebaseApp;
148
+ }
149
+ const serviceAccountPath = env.FIREBASE_SERVICE_ACCOUNT_PATH;
150
+ if (serviceAccountPath) {
151
+ const serviceAccount = __require(serviceAccountPath);
152
+ firebaseApp = admin.initializeApp({
153
+ credential: admin.credential.cert(serviceAccount),
154
+ projectId: env.FIREBASE_PROJECT_ID
155
+ });
156
+ } else if (env.FIREBASE_PROJECT_ID && env.FIREBASE_CLIENT_EMAIL && env.FIREBASE_PRIVATE_KEY) {
157
+ firebaseApp = admin.initializeApp({
158
+ credential: admin.credential.cert({
159
+ projectId: env.FIREBASE_PROJECT_ID,
160
+ clientEmail: env.FIREBASE_CLIENT_EMAIL,
161
+ privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n")
162
+ }),
163
+ projectId: env.FIREBASE_PROJECT_ID
164
+ });
165
+ } else if (env.FIREBASE_PROJECT_ID) {
166
+ firebaseApp = admin.initializeApp({
167
+ credential: admin.credential.applicationDefault(),
168
+ projectId: env.FIREBASE_PROJECT_ID
169
+ });
170
+ } else {
171
+ firebaseApp = admin.initializeApp({
172
+ projectId: "demo-project"
173
+ });
174
+ }
175
+ return firebaseApp;
176
+ }
177
+ function getFirestore() {
178
+ const app = initializeFirebase();
179
+ const db2 = app.firestore();
180
+ if (env.FIRESTORE_EMULATOR_HOST) {
181
+ db2.settings({
182
+ host: env.FIRESTORE_EMULATOR_HOST,
183
+ ssl: false
184
+ });
185
+ }
186
+ return db2;
187
+ }
188
+ function getAuth() {
189
+ const app = initializeFirebase();
190
+ return app.auth();
191
+ }
192
+ var db = getFirestore();
193
+ var wss = null;
194
+ function setupEventBroadcast(server, path = "/ws/events") {
195
+ if (wss) {
196
+ logger.warn("[WebSocket] Server already initialized");
197
+ return wss;
198
+ }
199
+ wss = new WebSocketServer({ server, path });
200
+ logger.info(`[WebSocket] Server listening at ${path}`);
201
+ wss.on("connection", (ws, req) => {
202
+ const clientId = req.headers["sec-websocket-key"] || "unknown";
203
+ logger.debug(`[WebSocket] Client connected: ${clientId}`);
204
+ ws.send(
205
+ JSON.stringify({
206
+ type: "CONNECTED",
207
+ timestamp: Date.now(),
208
+ message: "Connected to event stream"
209
+ })
210
+ );
211
+ ws.on("message", (data) => {
212
+ try {
213
+ const message = JSON.parse(data.toString());
214
+ logger.debug(`[WebSocket] Received from ${clientId}:`, message);
215
+ if (message.type && message.payload) {
216
+ serverEventBus.emit(message.type, message.payload, {
217
+ orbital: "client",
218
+ entity: clientId
219
+ });
220
+ }
221
+ } catch (error) {
222
+ logger.error(`[WebSocket] Failed to parse message:`, error);
223
+ }
224
+ });
225
+ ws.on("close", () => {
226
+ logger.debug(`[WebSocket] Client disconnected: ${clientId}`);
227
+ });
228
+ ws.on("error", (error) => {
229
+ logger.error(`[WebSocket] Client error:`, error);
230
+ });
231
+ });
232
+ serverEventBus.on("*", (event) => {
233
+ if (!wss) return;
234
+ const typedEvent = event;
235
+ const message = JSON.stringify({
236
+ type: typedEvent.type,
237
+ payload: typedEvent.payload,
238
+ timestamp: typedEvent.timestamp,
239
+ source: typedEvent.source
240
+ });
241
+ let broadcastCount = 0;
242
+ wss.clients.forEach((client) => {
243
+ if (client.readyState === WebSocket.OPEN) {
244
+ client.send(message);
245
+ broadcastCount++;
246
+ }
247
+ });
248
+ if (broadcastCount > 0) {
249
+ logger.debug(`[WebSocket] Broadcast ${typedEvent.type} to ${broadcastCount} client(s)`);
250
+ }
251
+ });
252
+ return wss;
253
+ }
254
+ function getWebSocketServer() {
255
+ return wss;
256
+ }
257
+ function closeWebSocketServer() {
258
+ return new Promise((resolve, reject) => {
259
+ if (!wss) {
260
+ resolve();
261
+ return;
262
+ }
263
+ wss.close((err) => {
264
+ if (err) {
265
+ reject(err);
266
+ } else {
267
+ wss = null;
268
+ resolve();
269
+ }
270
+ });
271
+ });
272
+ }
273
+ function getConnectedClientCount() {
274
+ if (!wss) return 0;
275
+ return wss.clients.size;
276
+ }
277
+ var AppError = class extends Error {
278
+ constructor(statusCode, message, code) {
279
+ super(message);
280
+ this.statusCode = statusCode;
281
+ this.message = message;
282
+ this.code = code;
283
+ this.name = "AppError";
284
+ }
285
+ };
286
+ var NotFoundError = class extends AppError {
287
+ constructor(message = "Resource not found") {
288
+ super(404, message, "NOT_FOUND");
289
+ }
290
+ };
291
+ var ValidationError = class extends AppError {
292
+ constructor(message = "Validation failed") {
293
+ super(400, message, "VALIDATION_ERROR");
294
+ }
295
+ };
296
+ var UnauthorizedError = class extends AppError {
297
+ constructor(message = "Unauthorized") {
298
+ super(401, message, "UNAUTHORIZED");
299
+ }
300
+ };
301
+ var ForbiddenError = class extends AppError {
302
+ constructor(message = "Forbidden") {
303
+ super(403, message, "FORBIDDEN");
304
+ }
305
+ };
306
+ var ConflictError = class extends AppError {
307
+ constructor(message = "Resource conflict") {
308
+ super(409, message, "CONFLICT");
309
+ }
310
+ };
311
+ var errorHandler = (err, _req, res, _next) => {
312
+ logger.error("Error:", { name: err.name, message: err.message, stack: err.stack });
313
+ if (err instanceof ZodError) {
314
+ res.status(400).json({
315
+ success: false,
316
+ error: "Validation failed",
317
+ code: "VALIDATION_ERROR",
318
+ details: err.errors.map((e) => ({
319
+ path: e.path.join("."),
320
+ message: e.message
321
+ }))
322
+ });
323
+ return;
324
+ }
325
+ if (err instanceof AppError) {
326
+ res.status(err.statusCode).json({
327
+ success: false,
328
+ error: err.message,
329
+ code: err.code
330
+ });
331
+ return;
332
+ }
333
+ if (err.name === "FirebaseError" || err.name === "FirestoreError") {
334
+ res.status(500).json({
335
+ success: false,
336
+ error: "Database error",
337
+ code: "DATABASE_ERROR"
338
+ });
339
+ return;
340
+ }
341
+ res.status(500).json({
342
+ success: false,
343
+ error: "Internal server error",
344
+ code: "INTERNAL_ERROR"
345
+ });
346
+ };
347
+ var asyncHandler = (fn) => (req, res, next) => {
348
+ Promise.resolve(fn(req, res, next)).catch(next);
349
+ };
350
+ var notFoundHandler = (req, res) => {
351
+ res.status(404).json({
352
+ success: false,
353
+ error: `Route ${req.method} ${req.path} not found`,
354
+ code: "ROUTE_NOT_FOUND"
355
+ });
356
+ };
357
+ var validateBody = (schema) => async (req, res, next) => {
358
+ try {
359
+ req.body = await schema.parseAsync(req.body);
360
+ next();
361
+ } catch (error) {
362
+ if (error instanceof ZodError) {
363
+ res.status(400).json({
364
+ success: false,
365
+ error: "Validation failed",
366
+ code: "VALIDATION_ERROR",
367
+ details: error.errors.map((e) => ({
368
+ path: e.path.join("."),
369
+ message: e.message
370
+ }))
371
+ });
372
+ return;
373
+ }
374
+ next(error);
375
+ }
376
+ };
377
+ var validateQuery = (schema) => async (req, res, next) => {
378
+ try {
379
+ req.query = await schema.parseAsync(req.query);
380
+ next();
381
+ } catch (error) {
382
+ if (error instanceof ZodError) {
383
+ res.status(400).json({
384
+ success: false,
385
+ error: "Invalid query parameters",
386
+ code: "VALIDATION_ERROR",
387
+ details: error.errors.map((e) => ({
388
+ path: e.path.join("."),
389
+ message: e.message
390
+ }))
391
+ });
392
+ return;
393
+ }
394
+ next(error);
395
+ }
396
+ };
397
+ var validateParams = (schema) => async (req, res, next) => {
398
+ try {
399
+ req.params = await schema.parseAsync(req.params);
400
+ next();
401
+ } catch (error) {
402
+ if (error instanceof ZodError) {
403
+ res.status(400).json({
404
+ success: false,
405
+ error: "Invalid path parameters",
406
+ code: "VALIDATION_ERROR",
407
+ details: error.errors.map((e) => ({
408
+ path: e.path.join("."),
409
+ message: e.message
410
+ }))
411
+ });
412
+ return;
413
+ }
414
+ next(error);
415
+ }
416
+ };
417
+
418
+ // src/middleware/authenticateFirebase.ts
419
+ var BEARER_PREFIX = "Bearer ";
420
+ async function authenticateFirebase(req, res, next) {
421
+ try {
422
+ const authorization = req.headers.authorization;
423
+ if (!authorization || !authorization.startsWith(BEARER_PREFIX)) {
424
+ return res.status(401).json({ error: "Authorization header missing or malformed" });
425
+ }
426
+ const token = authorization.slice(BEARER_PREFIX.length);
427
+ const decodedToken = await getAuth().verifyIdToken(token);
428
+ req.firebaseUser = decodedToken;
429
+ res.locals.firebaseUser = decodedToken;
430
+ return next();
431
+ } catch (error) {
432
+ console.error("Firebase authentication failed:", error);
433
+ return res.status(401).json({ error: "Unauthorized" });
434
+ }
435
+ }
436
+ var MockDataService = class {
437
+ stores = /* @__PURE__ */ new Map();
438
+ schemas = /* @__PURE__ */ new Map();
439
+ idCounters = /* @__PURE__ */ new Map();
440
+ constructor() {
441
+ if (env.MOCK_SEED !== void 0) {
442
+ faker.seed(env.MOCK_SEED);
443
+ logger.info(`[Mock] Using seed: ${env.MOCK_SEED}`);
444
+ }
445
+ }
446
+ // ============================================================================
447
+ // Store Management
448
+ // ============================================================================
449
+ /**
450
+ * Initialize store for an entity.
451
+ */
452
+ getStore(entityName) {
453
+ const normalized = entityName.toLowerCase();
454
+ if (!this.stores.has(normalized)) {
455
+ this.stores.set(normalized, /* @__PURE__ */ new Map());
456
+ this.idCounters.set(normalized, 0);
457
+ }
458
+ return this.stores.get(normalized);
459
+ }
460
+ /**
461
+ * Generate next ID for an entity.
462
+ */
463
+ nextId(entityName) {
464
+ const normalized = entityName.toLowerCase();
465
+ const counter = (this.idCounters.get(normalized) ?? 0) + 1;
466
+ this.idCounters.set(normalized, counter);
467
+ return `mock-${normalized}-${counter}`;
468
+ }
469
+ // ============================================================================
470
+ // Schema & Seeding
471
+ // ============================================================================
472
+ /**
473
+ * Register an entity schema.
474
+ */
475
+ registerSchema(entityName, schema) {
476
+ this.schemas.set(entityName.toLowerCase(), schema);
477
+ }
478
+ /**
479
+ * Seed an entity with mock data.
480
+ */
481
+ seed(entityName, fields, count = 10) {
482
+ const store = this.getStore(entityName);
483
+ const normalized = entityName.toLowerCase();
484
+ logger.info(`[Mock] Seeding ${count} ${entityName}...`);
485
+ for (let i = 0; i < count; i++) {
486
+ const item = this.generateMockItem(normalized, fields, i + 1);
487
+ store.set(item.id, item);
488
+ }
489
+ }
490
+ /**
491
+ * Generate a single mock item based on field schemas.
492
+ */
493
+ generateMockItem(entityName, fields, index) {
494
+ const id = this.nextId(entityName);
495
+ const now = /* @__PURE__ */ new Date();
496
+ const item = {
497
+ id,
498
+ createdAt: faker.date.past({ years: 1 }),
499
+ updatedAt: now
500
+ };
501
+ for (const field of fields) {
502
+ if (field.name === "id" || field.name === "createdAt" || field.name === "updatedAt") {
503
+ continue;
504
+ }
505
+ item[field.name] = this.generateFieldValue(entityName, field, index);
506
+ }
507
+ return item;
508
+ }
509
+ /**
510
+ * Generate a mock value for a field based on its schema.
511
+ */
512
+ generateFieldValue(entityName, field, index) {
513
+ if (!field.required && Math.random() > 0.8) {
514
+ return void 0;
515
+ }
516
+ switch (field.type) {
517
+ case "string":
518
+ return this.generateStringValue(entityName, field, index);
519
+ case "number":
520
+ return faker.number.int({
521
+ min: field.min ?? 0,
522
+ max: field.max ?? 1e3
523
+ });
524
+ case "boolean":
525
+ return faker.datatype.boolean();
526
+ case "date":
527
+ return this.generateDateValue(field);
528
+ case "enum":
529
+ if (field.enumValues && field.enumValues.length > 0) {
530
+ return faker.helpers.arrayElement(field.enumValues);
531
+ }
532
+ return null;
533
+ case "relation":
534
+ if (field.relatedEntity) {
535
+ const relatedStore = this.stores.get(field.relatedEntity.toLowerCase());
536
+ if (relatedStore && relatedStore.size > 0) {
537
+ const ids = Array.from(relatedStore.keys());
538
+ return faker.helpers.arrayElement(ids);
539
+ }
540
+ }
541
+ return null;
542
+ case "array":
543
+ return [];
544
+ default:
545
+ return null;
546
+ }
547
+ }
548
+ /**
549
+ * Generate a string value based on field name heuristics.
550
+ * Generic name/title fields use entity-aware format (e.g., "Project Name 1").
551
+ * Specific fields (email, phone, etc.) use faker.
552
+ */
553
+ generateStringValue(entityName, field, index) {
554
+ const name = field.name.toLowerCase();
555
+ if (field.enumValues && field.enumValues.length > 0) {
556
+ return faker.helpers.arrayElement(field.enumValues);
557
+ }
558
+ if (name.includes("email")) return faker.internet.email();
559
+ if (name.includes("phone")) return faker.phone.number();
560
+ if (name.includes("address")) return faker.location.streetAddress();
561
+ if (name.includes("city")) return faker.location.city();
562
+ if (name.includes("country")) return faker.location.country();
563
+ if (name.includes("url") || name.includes("website")) return faker.internet.url();
564
+ if (name.includes("avatar") || name.includes("image")) return faker.image.avatar();
565
+ if (name.includes("color")) return faker.color.human();
566
+ if (name.includes("uuid")) return faker.string.uuid();
567
+ const entityLabel = this.capitalizeFirst(entityName);
568
+ const fieldLabel = this.capitalizeFirst(field.name);
569
+ return `${entityLabel} ${fieldLabel} ${index}`;
570
+ }
571
+ /**
572
+ * Capitalize first letter of a string.
573
+ */
574
+ capitalizeFirst(str) {
575
+ return str.charAt(0).toUpperCase() + str.slice(1);
576
+ }
577
+ /**
578
+ * Generate a date value based on field name heuristics.
579
+ */
580
+ generateDateValue(field) {
581
+ const name = field.name.toLowerCase();
582
+ if (name.includes("created") || name.includes("start") || name.includes("birth")) {
583
+ return faker.date.past({ years: 2 });
584
+ }
585
+ if (name.includes("updated") || name.includes("modified")) {
586
+ return faker.date.recent({ days: 30 });
587
+ }
588
+ if (name.includes("deadline") || name.includes("due") || name.includes("end") || name.includes("expires")) {
589
+ return faker.date.future({ years: 1 });
590
+ }
591
+ return faker.date.anytime();
592
+ }
593
+ // ============================================================================
594
+ // CRUD Operations
595
+ // ============================================================================
596
+ /**
597
+ * List all items of an entity.
598
+ */
599
+ list(entityName) {
600
+ const store = this.getStore(entityName);
601
+ return Array.from(store.values());
602
+ }
603
+ /**
604
+ * Get a single item by ID.
605
+ */
606
+ getById(entityName, id) {
607
+ const store = this.getStore(entityName);
608
+ const item = store.get(id);
609
+ return item ?? null;
610
+ }
611
+ /**
612
+ * Create a new item.
613
+ */
614
+ create(entityName, data) {
615
+ const store = this.getStore(entityName);
616
+ const id = this.nextId(entityName);
617
+ const now = /* @__PURE__ */ new Date();
618
+ const item = {
619
+ ...data,
620
+ id,
621
+ createdAt: now,
622
+ updatedAt: now
623
+ };
624
+ store.set(id, item);
625
+ return item;
626
+ }
627
+ /**
628
+ * Update an existing item.
629
+ */
630
+ update(entityName, id, data) {
631
+ const store = this.getStore(entityName);
632
+ const existing = store.get(id);
633
+ if (!existing) {
634
+ return null;
635
+ }
636
+ const updated = {
637
+ ...existing,
638
+ ...data,
639
+ id,
640
+ // Preserve original ID
641
+ updatedAt: /* @__PURE__ */ new Date()
642
+ };
643
+ store.set(id, updated);
644
+ return updated;
645
+ }
646
+ /**
647
+ * Delete an item.
648
+ */
649
+ delete(entityName, id) {
650
+ const store = this.getStore(entityName);
651
+ if (!store.has(id)) {
652
+ return false;
653
+ }
654
+ store.delete(id);
655
+ return true;
656
+ }
657
+ // ============================================================================
658
+ // Utilities
659
+ // ============================================================================
660
+ /**
661
+ * Clear all data for an entity.
662
+ */
663
+ clear(entityName) {
664
+ const normalized = entityName.toLowerCase();
665
+ this.stores.delete(normalized);
666
+ this.idCounters.delete(normalized);
667
+ }
668
+ /**
669
+ * Clear all data.
670
+ */
671
+ clearAll() {
672
+ this.stores.clear();
673
+ this.idCounters.clear();
674
+ }
675
+ /**
676
+ * Get count of items for an entity.
677
+ */
678
+ count(entityName) {
679
+ const store = this.getStore(entityName);
680
+ return store.size;
681
+ }
682
+ };
683
+ var mockDataService = new MockDataService();
684
+
685
+ // src/utils/queryFilters.ts
686
+ var OPERATOR_MAP = {
687
+ "eq": "==",
688
+ "neq": "!=",
689
+ "gt": ">",
690
+ "gte": ">=",
691
+ "lt": "<",
692
+ "lte": "<=",
693
+ "contains": "array-contains",
694
+ "contains_any": "array-contains-any",
695
+ "in": "in",
696
+ "not_in": "not-in",
697
+ // Date operators map to same comparison operators
698
+ "date_eq": "==",
699
+ "date_gte": ">=",
700
+ "date_lte": "<="
701
+ };
702
+ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
703
+ "page",
704
+ "pageSize",
705
+ "limit",
706
+ "offset",
707
+ "search",
708
+ "q",
709
+ "sortBy",
710
+ "sortOrder",
711
+ "orderBy",
712
+ "orderDirection"
713
+ ]);
714
+ function parseQueryFilters(query) {
715
+ const filters = [];
716
+ for (const [key, value] of Object.entries(query)) {
717
+ if (RESERVED_PARAMS.has(key)) continue;
718
+ if (value === void 0 || value === null || value === "") continue;
719
+ const match = key.match(/^(.+)__(\w+)$/);
720
+ if (match) {
721
+ const [, field, op] = match;
722
+ const firestoreOp = OPERATOR_MAP[op];
723
+ if (firestoreOp) {
724
+ filters.push({
725
+ field,
726
+ operator: firestoreOp,
727
+ value: parseValue(value, op)
728
+ });
729
+ } else {
730
+ filters.push({
731
+ field: key,
732
+ operator: "==",
733
+ value: parseValue(value, "eq")
734
+ });
735
+ }
736
+ } else {
737
+ filters.push({
738
+ field: key,
739
+ operator: "==",
740
+ value: parseValue(value, "eq")
741
+ });
742
+ }
743
+ }
744
+ return filters;
745
+ }
746
+ function parseValue(value, operator) {
747
+ if (operator === "in" || operator === "not_in" || operator === "contains_any") {
748
+ if (typeof value === "string") {
749
+ return value.split(",").map((v) => v.trim());
750
+ }
751
+ if (Array.isArray(value)) {
752
+ return value;
753
+ }
754
+ return [value];
755
+ }
756
+ if (typeof value === "string") {
757
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
758
+ const num = parseFloat(value);
759
+ if (!isNaN(num)) {
760
+ return num;
761
+ }
762
+ }
763
+ if (value === "true") return true;
764
+ if (value === "false") return false;
765
+ }
766
+ return value;
767
+ }
768
+ function applyFiltersToQuery(collection, filters) {
769
+ let query = collection;
770
+ for (const filter of filters) {
771
+ query = query.where(
772
+ filter.field,
773
+ filter.operator,
774
+ filter.value
775
+ );
776
+ }
777
+ return query;
778
+ }
779
+ function extractPaginationParams(query, defaults = {}) {
780
+ return {
781
+ page: parseInt(query.page, 10) || defaults.page || 1,
782
+ pageSize: parseInt(query.pageSize, 10) || parseInt(query.limit, 10) || defaults.pageSize || 20,
783
+ sortBy: query.sortBy || query.orderBy,
784
+ sortOrder: query.sortOrder || query.orderDirection || defaults.sortOrder || "asc"
785
+ };
786
+ }
787
+
788
+ // src/services/DataService.ts
789
+ function applyFilterCondition(value, operator, filterValue) {
790
+ if (value === null || value === void 0) {
791
+ return operator === "!=" ? filterValue !== null : false;
792
+ }
793
+ switch (operator) {
794
+ case "==":
795
+ return value === filterValue;
796
+ case "!=":
797
+ return value !== filterValue;
798
+ case ">":
799
+ return value > filterValue;
800
+ case ">=":
801
+ return value >= filterValue;
802
+ case "<":
803
+ return value < filterValue;
804
+ case "<=":
805
+ return value <= filterValue;
806
+ case "array-contains":
807
+ return Array.isArray(value) && value.includes(filterValue);
808
+ case "array-contains-any":
809
+ return Array.isArray(value) && Array.isArray(filterValue) && filterValue.some((v) => value.includes(v));
810
+ case "in":
811
+ return Array.isArray(filterValue) && filterValue.includes(value);
812
+ case "not-in":
813
+ return Array.isArray(filterValue) && !filterValue.includes(value);
814
+ default:
815
+ return true;
816
+ }
817
+ }
818
+ var MockDataServiceAdapter = class {
819
+ async list(collection) {
820
+ return mockDataService.list(collection);
821
+ }
822
+ async listPaginated(collection, options = {}) {
823
+ const {
824
+ page = 1,
825
+ pageSize = 20,
826
+ search,
827
+ searchFields,
828
+ sortBy,
829
+ sortOrder = "asc",
830
+ filters
831
+ } = options;
832
+ let items = mockDataService.list(collection);
833
+ if (filters && filters.length > 0) {
834
+ items = items.filter((item) => {
835
+ const record = item;
836
+ return filters.every((filter) => {
837
+ const value = record[filter.field];
838
+ return applyFilterCondition(value, filter.operator, filter.value);
839
+ });
840
+ });
841
+ }
842
+ if (search && search.trim()) {
843
+ const searchLower = search.toLowerCase();
844
+ items = items.filter((item) => {
845
+ const record = item;
846
+ const fieldsToSearch = searchFields || Object.keys(record);
847
+ return fieldsToSearch.some((field) => {
848
+ const value = record[field];
849
+ if (value === null || value === void 0) return false;
850
+ return String(value).toLowerCase().includes(searchLower);
851
+ });
852
+ });
853
+ }
854
+ if (sortBy) {
855
+ items = [...items].sort((a, b) => {
856
+ const aVal = a[sortBy];
857
+ const bVal = b[sortBy];
858
+ if (aVal === bVal) return 0;
859
+ if (aVal === null || aVal === void 0) return 1;
860
+ if (bVal === null || bVal === void 0) return -1;
861
+ const comparison = aVal < bVal ? -1 : 1;
862
+ return sortOrder === "asc" ? comparison : -comparison;
863
+ });
864
+ }
865
+ const total = items.length;
866
+ const totalPages = Math.ceil(total / pageSize);
867
+ const startIndex = (page - 1) * pageSize;
868
+ const data = items.slice(startIndex, startIndex + pageSize);
869
+ return { data, total, page, pageSize, totalPages };
870
+ }
871
+ async getById(collection, id) {
872
+ return mockDataService.getById(collection, id);
873
+ }
874
+ async create(collection, data) {
875
+ return mockDataService.create(collection, data);
876
+ }
877
+ async update(collection, id, data) {
878
+ return mockDataService.update(collection, id, data);
879
+ }
880
+ async delete(collection, id) {
881
+ return mockDataService.delete(collection, id);
882
+ }
883
+ };
884
+ var FirebaseDataService = class {
885
+ async list(collection) {
886
+ const snapshot = await db.collection(collection).get();
887
+ return snapshot.docs.map((doc) => ({
888
+ id: doc.id,
889
+ ...doc.data()
890
+ }));
891
+ }
892
+ async listPaginated(collection, options = {}) {
893
+ const {
894
+ page = 1,
895
+ pageSize = 20,
896
+ search,
897
+ searchFields,
898
+ sortBy,
899
+ sortOrder = "asc",
900
+ filters
901
+ } = options;
902
+ let query = db.collection(collection);
903
+ if (filters && filters.length > 0) {
904
+ query = applyFiltersToQuery(query, filters);
905
+ }
906
+ if (sortBy && !search) {
907
+ query = query.orderBy(sortBy, sortOrder);
908
+ }
909
+ const snapshot = await query.get();
910
+ let items = snapshot.docs.map((doc) => ({
911
+ id: doc.id,
912
+ ...doc.data()
913
+ }));
914
+ if (search && search.trim()) {
915
+ const searchLower = search.toLowerCase();
916
+ items = items.filter((item) => {
917
+ const record = item;
918
+ const fieldsToSearch = searchFields || Object.keys(record);
919
+ return fieldsToSearch.some((field) => {
920
+ const value = record[field];
921
+ if (value === null || value === void 0) return false;
922
+ return String(value).toLowerCase().includes(searchLower);
923
+ });
924
+ });
925
+ }
926
+ if (sortBy && search) {
927
+ items = [...items].sort((a, b) => {
928
+ const aVal = a[sortBy];
929
+ const bVal = b[sortBy];
930
+ if (aVal === bVal) return 0;
931
+ if (aVal === null || aVal === void 0) return 1;
932
+ if (bVal === null || bVal === void 0) return -1;
933
+ const comparison = aVal < bVal ? -1 : 1;
934
+ return sortOrder === "asc" ? comparison : -comparison;
935
+ });
936
+ }
937
+ const total = items.length;
938
+ const totalPages = Math.ceil(total / pageSize);
939
+ const startIndex = (page - 1) * pageSize;
940
+ const data = items.slice(startIndex, startIndex + pageSize);
941
+ return { data, total, page, pageSize, totalPages };
942
+ }
943
+ async getById(collection, id) {
944
+ const doc = await db.collection(collection).doc(id).get();
945
+ if (!doc.exists) {
946
+ return null;
947
+ }
948
+ return { id: doc.id, ...doc.data() };
949
+ }
950
+ async create(collection, data) {
951
+ const now = /* @__PURE__ */ new Date();
952
+ const docRef = await db.collection(collection).add({
953
+ ...data,
954
+ createdAt: now,
955
+ updatedAt: now
956
+ });
957
+ return {
958
+ ...data,
959
+ id: docRef.id,
960
+ createdAt: now,
961
+ updatedAt: now
962
+ };
963
+ }
964
+ async update(collection, id, data) {
965
+ const docRef = db.collection(collection).doc(id);
966
+ const doc = await docRef.get();
967
+ if (!doc.exists) {
968
+ return null;
969
+ }
970
+ const now = /* @__PURE__ */ new Date();
971
+ await docRef.update({
972
+ ...data,
973
+ updatedAt: now
974
+ });
975
+ return {
976
+ ...doc.data(),
977
+ ...data,
978
+ id,
979
+ updatedAt: now
980
+ };
981
+ }
982
+ async delete(collection, id) {
983
+ const docRef = db.collection(collection).doc(id);
984
+ const doc = await docRef.get();
985
+ if (!doc.exists) {
986
+ return false;
987
+ }
988
+ await docRef.delete();
989
+ return true;
990
+ }
991
+ };
992
+ function createDataService() {
993
+ if (env.USE_MOCK_DATA) {
994
+ logger.info("[DataService] Using MockDataService");
995
+ return new MockDataServiceAdapter();
996
+ }
997
+ logger.info("[DataService] Using FirebaseDataService");
998
+ return new FirebaseDataService();
999
+ }
1000
+ var dataService = createDataService();
1001
+ function seedMockData(entities) {
1002
+ if (!env.USE_MOCK_DATA) {
1003
+ logger.info("[DataService] Mock mode disabled, skipping seed");
1004
+ return;
1005
+ }
1006
+ logger.info("[DataService] Seeding mock data...");
1007
+ for (const entity of entities) {
1008
+ mockDataService.seed(entity.name, entity.fields, entity.seedCount);
1009
+ }
1010
+ logger.info("[DataService] Mock data seeding complete");
1011
+ }
1012
+
1013
+ export { AppError, ConflictError, EventBus, ForbiddenError, MockDataService, NotFoundError, UnauthorizedError, ValidationError, applyFiltersToQuery, asyncHandler, authenticateFirebase, closeWebSocketServer, dataService, db, emitEntityEvent, env, errorHandler, extractPaginationParams, getAuth, getConnectedClientCount, getFirestore, getWebSocketServer, logger, mockDataService, notFoundHandler, parseQueryFilters, seedMockData, serverEventBus, setupEventBroadcast, validateBody, validateParams, validateQuery };
1014
+ //# sourceMappingURL=index.js.map
1015
+ //# sourceMappingURL=index.js.map