@almadar/server 2.1.1 → 2.1.3
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/contracts.d.ts +174 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/deepagent/memory.d.ts +16 -0
- package/dist/deepagent/memory.d.ts.map +1 -0
- package/dist/deepagent/session.d.ts +16 -0
- package/dist/deepagent/session.d.ts.map +1 -0
- package/dist/deepagent/skill-agent.d.ts +22 -0
- package/dist/deepagent/skill-agent.d.ts.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2616 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/db.d.ts +36 -0
- package/dist/lib/db.d.ts.map +1 -0
- package/dist/lib/debugRouter.d.ts +21 -0
- package/dist/lib/debugRouter.d.ts.map +1 -0
- package/dist/lib/env.d.ts +16 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/eventBus.d.ts +44 -0
- package/dist/lib/eventBus.d.ts.map +1 -0
- package/dist/lib/eventBusTransport.d.ts +143 -0
- package/dist/lib/eventBusTransport.d.ts.map +1 -0
- package/dist/lib/eventPersistence.d.ts +151 -0
- package/dist/lib/eventPersistence.d.ts.map +1 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +288 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.d.ts +7 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/serviceDiscovery.d.ts +168 -0
- package/dist/lib/serviceDiscovery.d.ts.map +1 -0
- package/dist/lib/websocket.d.ts +41 -0
- package/dist/lib/websocket.d.ts.map +1 -0
- package/dist/middleware/authenticateFirebase.d.ts +4 -0
- package/dist/middleware/authenticateFirebase.d.ts.map +1 -0
- package/dist/middleware/errorHandler.d.ts +53 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +284 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/multi-user.d.ts +34 -0
- package/dist/middleware/multi-user.d.ts.map +1 -0
- package/dist/middleware/validation.d.ts +15 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/routes/observability.d.ts +11 -0
- package/dist/routes/observability.d.ts.map +1 -0
- package/dist/services/DataService.d.ts +70 -0
- package/dist/services/DataService.d.ts.map +1 -0
- package/dist/services/MockDataService.d.ts +110 -0
- package/dist/services/MockDataService.d.ts.map +1 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +735 -0
- package/dist/services/index.js.map +1 -0
- package/dist/stores/ChangeSetStore.d.ts +24 -0
- package/dist/stores/ChangeSetStore.d.ts.map +1 -0
- package/dist/stores/SchemaProtectionService.d.ts +23 -0
- package/dist/stores/SchemaProtectionService.d.ts.map +1 -0
- package/dist/stores/SchemaStore.d.ts +52 -0
- package/dist/stores/SchemaStore.d.ts.map +1 -0
- package/dist/stores/SnapshotStore.d.ts +26 -0
- package/dist/stores/SnapshotStore.d.ts.map +1 -0
- package/dist/stores/ValidationStore.d.ts +19 -0
- package/dist/stores/ValidationStore.d.ts.map +1 -0
- package/dist/stores/firestoreFormat.d.ts +21 -0
- package/dist/stores/firestoreFormat.d.ts.map +1 -0
- package/dist/stores/index.d.ts +14 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +519 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +106 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/queryFilters.d.ts +87 -0
- package/dist/utils/queryFilters.d.ts.map +1 -0
- package/dist/websocket/state-sync.d.ts +39 -0
- package/dist/websocket/state-sync.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,2616 @@
|
|
|
1
|
+
import admin from 'firebase-admin';
|
|
2
|
+
export { default as admin } from 'firebase-admin';
|
|
3
|
+
import { Router } from 'express';
|
|
4
|
+
import { z, ZodError } from 'zod';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import { faker } from '@faker-js/faker';
|
|
7
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
+
import { diffSchemas, categorizeRemovals, detectPageContentReduction, isDestructiveChange, hasSignificantPageReduction, requiresConfirmation } from '@almadar/core';
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
13
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
14
|
+
}) : x)(function(x) {
|
|
15
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
16
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
17
|
+
});
|
|
18
|
+
var __esm = (fn, res) => function __init() {
|
|
19
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
20
|
+
};
|
|
21
|
+
var __export = (target, all) => {
|
|
22
|
+
for (var name in all)
|
|
23
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
|
+
};
|
|
25
|
+
function initializeFirebase() {
|
|
26
|
+
if (admin.apps.length > 0) {
|
|
27
|
+
return admin.app();
|
|
28
|
+
}
|
|
29
|
+
const projectId = process.env.FIREBASE_PROJECT_ID;
|
|
30
|
+
const emulatorHost = process.env.FIRESTORE_EMULATOR_HOST;
|
|
31
|
+
if (emulatorHost) {
|
|
32
|
+
const app = admin.initializeApp({
|
|
33
|
+
projectId: projectId || "demo-project"
|
|
34
|
+
});
|
|
35
|
+
console.log(`Firebase Admin initialized for emulator: ${emulatorHost}`);
|
|
36
|
+
return app;
|
|
37
|
+
}
|
|
38
|
+
const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH;
|
|
39
|
+
if (serviceAccountPath) {
|
|
40
|
+
const serviceAccount = __require(serviceAccountPath);
|
|
41
|
+
return admin.initializeApp({
|
|
42
|
+
credential: admin.credential.cert(serviceAccount),
|
|
43
|
+
projectId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
|
|
47
|
+
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
|
|
48
|
+
if (projectId && clientEmail && privateKey) {
|
|
49
|
+
return admin.initializeApp({
|
|
50
|
+
credential: admin.credential.cert({
|
|
51
|
+
projectId,
|
|
52
|
+
clientEmail,
|
|
53
|
+
privateKey: privateKey.replace(/\\n/g, "\n")
|
|
54
|
+
}),
|
|
55
|
+
projectId
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (projectId) {
|
|
59
|
+
return admin.initializeApp({
|
|
60
|
+
credential: admin.credential.applicationDefault(),
|
|
61
|
+
projectId
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
throw new Error(
|
|
65
|
+
"@almadar/server: Cannot initialize Firebase \u2014 no credentials found. Set FIREBASE_PROJECT_ID + FIREBASE_CLIENT_EMAIL + FIREBASE_PRIVATE_KEY, or FIREBASE_SERVICE_ACCOUNT_PATH, or FIRESTORE_EMULATOR_HOST."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function getApp() {
|
|
69
|
+
if (admin.apps.length === 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"@almadar/server: Firebase Admin SDK is not initialized. Call initializeFirebase() or admin.initializeApp() before using @almadar/server."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return admin.app();
|
|
75
|
+
}
|
|
76
|
+
function getFirestore() {
|
|
77
|
+
return getApp().firestore();
|
|
78
|
+
}
|
|
79
|
+
function getAuth() {
|
|
80
|
+
return getApp().auth();
|
|
81
|
+
}
|
|
82
|
+
var db;
|
|
83
|
+
var init_db = __esm({
|
|
84
|
+
"src/lib/db.ts"() {
|
|
85
|
+
db = new Proxy({}, {
|
|
86
|
+
get(_target, prop, receiver) {
|
|
87
|
+
const firestore = getFirestore();
|
|
88
|
+
const value = Reflect.get(firestore, prop, receiver);
|
|
89
|
+
return typeof value === "function" ? value.bind(firestore) : value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// src/deepagent/memory.ts
|
|
96
|
+
var memory_exports = {};
|
|
97
|
+
__export(memory_exports, {
|
|
98
|
+
getMemoryManager: () => getMemoryManager,
|
|
99
|
+
resetMemoryManager: () => resetMemoryManager
|
|
100
|
+
});
|
|
101
|
+
async function getMemoryManager() {
|
|
102
|
+
if (!memoryManager) {
|
|
103
|
+
const { MemoryManager } = await import('@almadar-io/agent');
|
|
104
|
+
memoryManager = new MemoryManager({
|
|
105
|
+
db,
|
|
106
|
+
usersCollection: "agent_memory_users",
|
|
107
|
+
projectsCollection: "agent_memory_projects",
|
|
108
|
+
generationsCollection: "agent_memory_generations",
|
|
109
|
+
patternsCollection: "agent_memory_patterns",
|
|
110
|
+
interruptsCollection: "agent_memory_interrupts",
|
|
111
|
+
feedbackCollection: "agent_memory_feedback",
|
|
112
|
+
checkpointsCollection: "agent_memory_checkpoints",
|
|
113
|
+
toolPreferencesCollection: "agent_memory_tool_preferences"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return memoryManager;
|
|
117
|
+
}
|
|
118
|
+
function resetMemoryManager() {
|
|
119
|
+
memoryManager = null;
|
|
120
|
+
}
|
|
121
|
+
var memoryManager;
|
|
122
|
+
var init_memory = __esm({
|
|
123
|
+
"src/deepagent/memory.ts"() {
|
|
124
|
+
init_db();
|
|
125
|
+
memoryManager = null;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// src/deepagent/session.ts
|
|
130
|
+
var session_exports = {};
|
|
131
|
+
__export(session_exports, {
|
|
132
|
+
getSessionManager: () => getSessionManager,
|
|
133
|
+
resetSessionManager: () => resetSessionManager
|
|
134
|
+
});
|
|
135
|
+
async function getSessionManager() {
|
|
136
|
+
if (!sessionManager) {
|
|
137
|
+
const { SessionManager } = await import('@almadar-io/agent');
|
|
138
|
+
const firestoreDb = db;
|
|
139
|
+
const memoryManager2 = await getMemoryManager();
|
|
140
|
+
sessionManager = new SessionManager({
|
|
141
|
+
mode: "firestore",
|
|
142
|
+
firestoreDb,
|
|
143
|
+
memoryManager: memoryManager2,
|
|
144
|
+
// Enable GAP-002D
|
|
145
|
+
compactionConfig: {
|
|
146
|
+
maxTokens: 15e4,
|
|
147
|
+
keepRecentMessages: 10,
|
|
148
|
+
strategy: "last"
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return sessionManager;
|
|
153
|
+
}
|
|
154
|
+
function resetSessionManager() {
|
|
155
|
+
sessionManager = null;
|
|
156
|
+
}
|
|
157
|
+
var sessionManager;
|
|
158
|
+
var init_session = __esm({
|
|
159
|
+
"src/deepagent/session.ts"() {
|
|
160
|
+
init_db();
|
|
161
|
+
init_memory();
|
|
162
|
+
sessionManager = null;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/deepagent/skill-agent.ts
|
|
167
|
+
var skill_agent_exports = {};
|
|
168
|
+
__export(skill_agent_exports, {
|
|
169
|
+
createServerSkillAgent: () => createServerSkillAgent,
|
|
170
|
+
getMemoryManager: () => getMemoryManager,
|
|
171
|
+
getSessionManager: () => getSessionManager
|
|
172
|
+
});
|
|
173
|
+
async function loadAgent() {
|
|
174
|
+
return import('@almadar-io/agent');
|
|
175
|
+
}
|
|
176
|
+
async function createServerSkillAgent(options) {
|
|
177
|
+
const agent = await loadAgent();
|
|
178
|
+
const memoryManager2 = await getMemoryManager();
|
|
179
|
+
await getSessionManager();
|
|
180
|
+
const observability = agent.getObservabilityCollector();
|
|
181
|
+
const multiUser = agent.getMultiUserManager();
|
|
182
|
+
if (options.threadId) {
|
|
183
|
+
const access = multiUser.canAccessSession(options.threadId, {
|
|
184
|
+
userId: options.userId,
|
|
185
|
+
roles: ["user"]
|
|
186
|
+
});
|
|
187
|
+
if (!access.allowed) {
|
|
188
|
+
throw new Error(`Access denied: ${access.reason}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
observability.startSession(options.threadId ?? "new", options.userId);
|
|
192
|
+
const workflowToolWrapper = agent.createWorkflowToolWrapper({
|
|
193
|
+
maxRetries: 2,
|
|
194
|
+
enableTelemetry: true,
|
|
195
|
+
timeoutMs: 3e5
|
|
196
|
+
// 5 minutes
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
const result = await agent.createSkillAgent({
|
|
200
|
+
...options,
|
|
201
|
+
memoryManager: memoryManager2,
|
|
202
|
+
// GAP-001: Enable memory
|
|
203
|
+
userId: options.userId,
|
|
204
|
+
// GAP-002D: Session → Memory sync
|
|
205
|
+
appId: options.appId,
|
|
206
|
+
toolWrapper: workflowToolWrapper.wrap
|
|
207
|
+
// Always use workflow wrapper for reliability
|
|
208
|
+
});
|
|
209
|
+
const threadId = result.threadId;
|
|
210
|
+
if (threadId) {
|
|
211
|
+
multiUser.assignSessionOwnership(threadId, options.userId);
|
|
212
|
+
}
|
|
213
|
+
observability.recordEvent({
|
|
214
|
+
type: "session_start",
|
|
215
|
+
sessionId: result.threadId,
|
|
216
|
+
userId: options.userId,
|
|
217
|
+
payload: { skill: options.skill }
|
|
218
|
+
});
|
|
219
|
+
return result;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
observability.recordError(options.threadId ?? "new", error);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
var init_skill_agent = __esm({
|
|
226
|
+
"src/deepagent/skill-agent.ts"() {
|
|
227
|
+
init_memory();
|
|
228
|
+
init_session();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// src/middleware/multi-user.ts
|
|
233
|
+
var multi_user_exports = {};
|
|
234
|
+
__export(multi_user_exports, {
|
|
235
|
+
multiUserMiddleware: () => multiUserMiddleware,
|
|
236
|
+
verifyFirebaseAuth: () => verifyFirebaseAuth
|
|
237
|
+
});
|
|
238
|
+
async function loadAgent2() {
|
|
239
|
+
return import('@almadar-io/agent');
|
|
240
|
+
}
|
|
241
|
+
async function multiUserMiddleware(req, res, next) {
|
|
242
|
+
const userId = req.user?.uid;
|
|
243
|
+
if (!userId) {
|
|
244
|
+
res.status(401).json({ error: "Authentication required" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const agent = await loadAgent2();
|
|
248
|
+
req.userContext = agent.createUserContext(userId, {
|
|
249
|
+
orgId: req.user?.orgId,
|
|
250
|
+
roles: req.user?.roles ?? ["user"]
|
|
251
|
+
});
|
|
252
|
+
const originalJson = res.json.bind(res);
|
|
253
|
+
res.json = (body) => {
|
|
254
|
+
if (body && typeof body === "object" && "threadId" in body) {
|
|
255
|
+
const multiUser = agent.getMultiUserManager();
|
|
256
|
+
multiUser.assignSessionOwnership(
|
|
257
|
+
body.threadId,
|
|
258
|
+
userId
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return originalJson(body);
|
|
262
|
+
};
|
|
263
|
+
next();
|
|
264
|
+
}
|
|
265
|
+
async function verifyFirebaseAuth(req, res, next) {
|
|
266
|
+
const authHeader = req.headers.authorization;
|
|
267
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
268
|
+
res.status(401).json({ error: "No token provided" });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const token = authHeader.split("Bearer ")[1];
|
|
272
|
+
try {
|
|
273
|
+
const decodedToken = await getAuth().verifyIdToken(token);
|
|
274
|
+
const user = await getAuth().getUser(decodedToken.uid);
|
|
275
|
+
req.user = {
|
|
276
|
+
uid: decodedToken.uid,
|
|
277
|
+
email: decodedToken.email,
|
|
278
|
+
roles: user.customClaims?.roles ?? ["user"],
|
|
279
|
+
orgId: user.customClaims?.orgId
|
|
280
|
+
};
|
|
281
|
+
next();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error("Token verification failed:", error);
|
|
284
|
+
res.status(401).json({ error: "Invalid token" });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
var init_multi_user = __esm({
|
|
288
|
+
"src/middleware/multi-user.ts"() {
|
|
289
|
+
init_db();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// src/websocket/state-sync.ts
|
|
294
|
+
var state_sync_exports = {};
|
|
295
|
+
__export(state_sync_exports, {
|
|
296
|
+
setupStateSyncWebSocket: () => setupStateSyncWebSocket
|
|
297
|
+
});
|
|
298
|
+
async function loadAgent3() {
|
|
299
|
+
return import('@almadar-io/agent');
|
|
300
|
+
}
|
|
301
|
+
async function setupStateSyncWebSocket(io) {
|
|
302
|
+
const agent = await loadAgent3();
|
|
303
|
+
const stateSync = agent.getStateSyncManager();
|
|
304
|
+
io.use(async (socket, next) => {
|
|
305
|
+
try {
|
|
306
|
+
const token = socket.handshake.auth.token;
|
|
307
|
+
if (!token) {
|
|
308
|
+
return next(new Error("Authentication required"));
|
|
309
|
+
}
|
|
310
|
+
const decodedToken = await getAuth().verifyIdToken(token);
|
|
311
|
+
const user = await getAuth().getUser(decodedToken.uid);
|
|
312
|
+
socket.data.user = {
|
|
313
|
+
uid: decodedToken.uid,
|
|
314
|
+
roles: user.customClaims?.roles ?? ["user"],
|
|
315
|
+
orgId: user.customClaims?.orgId
|
|
316
|
+
};
|
|
317
|
+
next();
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error("Socket auth failed:", error);
|
|
320
|
+
next(new Error("Invalid token"));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
io.on("connection", (socket) => {
|
|
324
|
+
const userId = socket.data.user.uid;
|
|
325
|
+
const clientId = socket.handshake.auth.clientId;
|
|
326
|
+
console.log(`[StateSync] Client ${clientId} connected for user ${userId}`);
|
|
327
|
+
stateSync.updateConfig({ clientId });
|
|
328
|
+
socket.join(`user:${userId}`);
|
|
329
|
+
socket.on("stateChange", (...args) => {
|
|
330
|
+
const event = args[0];
|
|
331
|
+
const multiUser = agent.getMultiUserManager();
|
|
332
|
+
if (!multiUser.isSessionOwner(event.threadId, userId)) {
|
|
333
|
+
socket.emit("error", { message: "Not session owner" });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
stateSync.receiveRemoteChange(event);
|
|
337
|
+
socket.to(`user:${userId}`).emit("remoteChange", event);
|
|
338
|
+
});
|
|
339
|
+
stateSync.on("syncRequired", (...args) => {
|
|
340
|
+
const changes = args[0];
|
|
341
|
+
socket.emit("syncBatch", changes);
|
|
342
|
+
});
|
|
343
|
+
socket.on("disconnect", () => {
|
|
344
|
+
console.log(`[StateSync] Client ${clientId} disconnected`);
|
|
345
|
+
socket.leave(`user:${userId}`);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
var init_state_sync = __esm({
|
|
350
|
+
"src/websocket/state-sync.ts"() {
|
|
351
|
+
init_db();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// src/routes/observability.ts
|
|
356
|
+
var observability_exports = {};
|
|
357
|
+
__export(observability_exports, {
|
|
358
|
+
default: () => observability_default
|
|
359
|
+
});
|
|
360
|
+
async function getObservabilityCollector() {
|
|
361
|
+
const mod = await import('@almadar-io/agent');
|
|
362
|
+
return mod.getObservabilityCollector();
|
|
363
|
+
}
|
|
364
|
+
var router, observability_default;
|
|
365
|
+
var init_observability = __esm({
|
|
366
|
+
"src/routes/observability.ts"() {
|
|
367
|
+
router = Router();
|
|
368
|
+
router.get("/metrics", async (req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const collector = await getObservabilityCollector();
|
|
371
|
+
const snapshot = collector.getPerformanceSnapshot();
|
|
372
|
+
res.json(snapshot);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error("Metrics error:", error);
|
|
375
|
+
res.status(500).json({ error: "Failed to get metrics" });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
router.get("/health", async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const collector = await getObservabilityCollector();
|
|
381
|
+
const health = await collector.healthCheck();
|
|
382
|
+
const allHealthy = health.every((h) => h.status === "healthy");
|
|
383
|
+
res.status(allHealthy ? 200 : 503).json({
|
|
384
|
+
status: allHealthy ? "healthy" : "degraded",
|
|
385
|
+
timestamp: Date.now(),
|
|
386
|
+
checks: health
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error("Health check error:", error);
|
|
390
|
+
res.status(500).json({
|
|
391
|
+
status: "unhealthy",
|
|
392
|
+
error: "Health check failed"
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
router.get("/sessions/:threadId/telemetry", async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const collector = await getObservabilityCollector();
|
|
399
|
+
const telemetry = collector.getSessionTelemetry(req.params.threadId);
|
|
400
|
+
if (!telemetry) {
|
|
401
|
+
res.status(404).json({ error: "Session not found" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
res.json(telemetry);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error("Telemetry error:", error);
|
|
407
|
+
res.status(500).json({ error: "Failed to get telemetry" });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
router.get("/active-sessions", async (req, res) => {
|
|
411
|
+
try {
|
|
412
|
+
const collector = await getObservabilityCollector();
|
|
413
|
+
const sessions = collector.getActiveSessions();
|
|
414
|
+
res.json(sessions);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error("Active sessions error:", error);
|
|
417
|
+
res.status(500).json({ error: "Failed to get active sessions" });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
observability_default = router;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
dotenv.config();
|
|
424
|
+
var envSchema = z.object({
|
|
425
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
426
|
+
PORT: z.string().default("3030").transform((val) => parseInt(val, 10)),
|
|
427
|
+
CORS_ORIGIN: z.string().default("http://localhost:5173").transform((val) => val.includes(",") ? val.split(",").map((s) => s.trim()) : val),
|
|
428
|
+
// Database (Prisma/SQL) - optional
|
|
429
|
+
DATABASE_URL: z.string().optional(),
|
|
430
|
+
// Firebase/Firestore configuration
|
|
431
|
+
FIREBASE_PROJECT_ID: z.string().optional(),
|
|
432
|
+
FIREBASE_CLIENT_EMAIL: z.string().optional(),
|
|
433
|
+
FIREBASE_PRIVATE_KEY: z.string().optional(),
|
|
434
|
+
FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),
|
|
435
|
+
FIRESTORE_EMULATOR_HOST: z.string().optional(),
|
|
436
|
+
FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
|
|
437
|
+
// API configuration
|
|
438
|
+
API_PREFIX: z.string().default("/api"),
|
|
439
|
+
// Mock data configuration
|
|
440
|
+
USE_MOCK_DATA: z.string().default("true").transform((v) => v === "true"),
|
|
441
|
+
MOCK_SEED: z.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
442
|
+
});
|
|
443
|
+
var parsed = envSchema.safeParse(process.env);
|
|
444
|
+
if (!parsed.success) {
|
|
445
|
+
console.error("\u274C Invalid environment variables:", parsed.error.flatten().fieldErrors);
|
|
446
|
+
throw new Error("Invalid environment variables");
|
|
447
|
+
}
|
|
448
|
+
var env = parsed.data;
|
|
449
|
+
|
|
450
|
+
// src/lib/logger.ts
|
|
451
|
+
var colors = {
|
|
452
|
+
debug: "\x1B[36m",
|
|
453
|
+
// Cyan
|
|
454
|
+
info: "\x1B[32m",
|
|
455
|
+
// Green
|
|
456
|
+
warn: "\x1B[33m",
|
|
457
|
+
// Yellow
|
|
458
|
+
error: "\x1B[31m",
|
|
459
|
+
// Red
|
|
460
|
+
reset: "\x1B[0m"
|
|
461
|
+
};
|
|
462
|
+
var shouldLog = (level) => {
|
|
463
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
464
|
+
const minLevel = env.NODE_ENV === "production" ? "info" : "debug";
|
|
465
|
+
return levels.indexOf(level) >= levels.indexOf(minLevel);
|
|
466
|
+
};
|
|
467
|
+
var formatMessage = (level, message, meta) => {
|
|
468
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
+
const color = colors[level];
|
|
470
|
+
const prefix = `${color}[${level.toUpperCase()}]${colors.reset}`;
|
|
471
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
472
|
+
return `${timestamp} ${prefix} ${message}${metaStr}`;
|
|
473
|
+
};
|
|
474
|
+
var logger = {
|
|
475
|
+
debug: (message, meta) => {
|
|
476
|
+
if (shouldLog("debug")) {
|
|
477
|
+
console.log(formatMessage("debug", message, meta));
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
info: (message, meta) => {
|
|
481
|
+
if (shouldLog("info")) {
|
|
482
|
+
console.log(formatMessage("info", message, meta));
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
warn: (message, meta) => {
|
|
486
|
+
if (shouldLog("warn")) {
|
|
487
|
+
console.warn(formatMessage("warn", message, meta));
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
error: (message, meta) => {
|
|
491
|
+
if (shouldLog("error")) {
|
|
492
|
+
console.error(formatMessage("error", message, meta));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/lib/eventBus.ts
|
|
498
|
+
var MAX_EVENT_LOG = 200;
|
|
499
|
+
var EventBus = class {
|
|
500
|
+
handlers = /* @__PURE__ */ new Map();
|
|
501
|
+
debug;
|
|
502
|
+
eventLog = [];
|
|
503
|
+
constructor(options) {
|
|
504
|
+
this.debug = options?.debug ?? false;
|
|
505
|
+
}
|
|
506
|
+
on(event, handler) {
|
|
507
|
+
if (!this.handlers.has(event)) {
|
|
508
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
509
|
+
}
|
|
510
|
+
this.handlers.get(event).add(handler);
|
|
511
|
+
return () => {
|
|
512
|
+
this.handlers.get(event)?.delete(handler);
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
off(event, handler) {
|
|
516
|
+
this.handlers.get(event)?.delete(handler);
|
|
517
|
+
}
|
|
518
|
+
emit(event, payload, meta) {
|
|
519
|
+
if (this.debug) {
|
|
520
|
+
console.log(`[EventBus] Emitting ${event}:`, payload);
|
|
521
|
+
}
|
|
522
|
+
const handlers = this.handlers.get(event);
|
|
523
|
+
const listenerCount = handlers?.size ?? 0;
|
|
524
|
+
if (handlers) {
|
|
525
|
+
handlers.forEach((handler) => {
|
|
526
|
+
try {
|
|
527
|
+
handler(payload, meta);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error(`[EventBus] Error in handler for ${event}:`, err);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
let wildcardListenerCount = 0;
|
|
534
|
+
if (event !== "*") {
|
|
535
|
+
const wildcardHandlers = this.handlers.get("*");
|
|
536
|
+
wildcardListenerCount = wildcardHandlers?.size ?? 0;
|
|
537
|
+
if (wildcardHandlers) {
|
|
538
|
+
wildcardHandlers.forEach((handler) => {
|
|
539
|
+
try {
|
|
540
|
+
handler({ type: event, payload, timestamp: Date.now() }, meta);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.error(`[EventBus] Error in wildcard handler for ${event}:`, err);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (this.debug) {
|
|
548
|
+
this.eventLog.push({
|
|
549
|
+
event,
|
|
550
|
+
payload,
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
listenerCount,
|
|
553
|
+
wildcardListenerCount
|
|
554
|
+
});
|
|
555
|
+
if (this.eventLog.length > MAX_EVENT_LOG) {
|
|
556
|
+
this.eventLog.splice(0, this.eventLog.length - MAX_EVENT_LOG);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
getRecentEvents(limit = 50) {
|
|
561
|
+
return this.eventLog.slice(-limit);
|
|
562
|
+
}
|
|
563
|
+
clearEventLog() {
|
|
564
|
+
this.eventLog.length = 0;
|
|
565
|
+
}
|
|
566
|
+
getListenerCounts() {
|
|
567
|
+
const counts = {};
|
|
568
|
+
this.handlers.forEach((handlers, event) => {
|
|
569
|
+
counts[event] = handlers.size;
|
|
570
|
+
});
|
|
571
|
+
return counts;
|
|
572
|
+
}
|
|
573
|
+
clear() {
|
|
574
|
+
this.handlers.clear();
|
|
575
|
+
this.eventLog.length = 0;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var _serverEventBus = null;
|
|
579
|
+
function getServerEventBus() {
|
|
580
|
+
if (!_serverEventBus) {
|
|
581
|
+
_serverEventBus = new EventBus({
|
|
582
|
+
debug: process.env.NODE_ENV === "development"
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return _serverEventBus;
|
|
586
|
+
}
|
|
587
|
+
function resetServerEventBus() {
|
|
588
|
+
_serverEventBus?.clear();
|
|
589
|
+
_serverEventBus = null;
|
|
590
|
+
}
|
|
591
|
+
function emitEntityEvent(entityType, action, payload) {
|
|
592
|
+
const eventType = `${entityType.toUpperCase()}_${action}`;
|
|
593
|
+
getServerEventBus().emit(eventType, payload, { orbital: entityType });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/lib/eventBusTransport.ts
|
|
597
|
+
var InMemoryTransport = class {
|
|
598
|
+
async publish() {
|
|
599
|
+
}
|
|
600
|
+
async subscribe() {
|
|
601
|
+
}
|
|
602
|
+
async close() {
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
var RedisTransport = class {
|
|
606
|
+
channel;
|
|
607
|
+
publishFn;
|
|
608
|
+
subscribeFn;
|
|
609
|
+
closeFn;
|
|
610
|
+
constructor(options) {
|
|
611
|
+
this.channel = options.channel ?? "almadar:events";
|
|
612
|
+
this.publishFn = options.publishFn;
|
|
613
|
+
this.subscribeFn = options.subscribeFn;
|
|
614
|
+
this.closeFn = options.closeFn;
|
|
615
|
+
}
|
|
616
|
+
async publish(message) {
|
|
617
|
+
const serialized = JSON.stringify(message);
|
|
618
|
+
await this.publishFn(this.channel, serialized);
|
|
619
|
+
}
|
|
620
|
+
async subscribe(receiver) {
|
|
621
|
+
await this.subscribeFn(this.channel, (raw) => {
|
|
622
|
+
try {
|
|
623
|
+
const message = JSON.parse(raw);
|
|
624
|
+
receiver(message);
|
|
625
|
+
} catch {
|
|
626
|
+
console.error("[RedisTransport] Failed to parse message:", raw);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
async close() {
|
|
631
|
+
if (this.closeFn) {
|
|
632
|
+
await this.closeFn();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var instanceCounter = 0;
|
|
637
|
+
var DistributedEventBus = class {
|
|
638
|
+
localBus;
|
|
639
|
+
transport;
|
|
640
|
+
instanceId;
|
|
641
|
+
isRelaying = false;
|
|
642
|
+
constructor(options) {
|
|
643
|
+
this.localBus = new EventBus({ debug: options?.debug });
|
|
644
|
+
this.transport = options?.transport ?? new InMemoryTransport();
|
|
645
|
+
this.instanceId = `instance_${++instanceCounter}_${Date.now()}`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Initialize the transport subscription. Call once at startup.
|
|
649
|
+
*/
|
|
650
|
+
async connect() {
|
|
651
|
+
await this.transport.subscribe((message) => {
|
|
652
|
+
if (message.sourceId === this.instanceId) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
this.isRelaying = true;
|
|
656
|
+
try {
|
|
657
|
+
this.localBus.emit(message.event, message.payload, message.meta);
|
|
658
|
+
} finally {
|
|
659
|
+
this.isRelaying = false;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Emit an event locally and publish to transport for other processes.
|
|
665
|
+
*/
|
|
666
|
+
emit(event, payload, meta) {
|
|
667
|
+
this.localBus.emit(event, payload, meta);
|
|
668
|
+
if (!this.isRelaying) {
|
|
669
|
+
const message = {
|
|
670
|
+
event,
|
|
671
|
+
payload,
|
|
672
|
+
meta,
|
|
673
|
+
sourceId: this.instanceId,
|
|
674
|
+
timestamp: Date.now()
|
|
675
|
+
};
|
|
676
|
+
this.transport.publish(message).catch((err) => {
|
|
677
|
+
console.error("[DistributedEventBus] Transport publish error:", err);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/** Subscribe to an event */
|
|
682
|
+
on(event, handler) {
|
|
683
|
+
return this.localBus.on(event, handler);
|
|
684
|
+
}
|
|
685
|
+
/** Unsubscribe from an event */
|
|
686
|
+
off(event, handler) {
|
|
687
|
+
this.localBus.off(event, handler);
|
|
688
|
+
}
|
|
689
|
+
/** Get recent events (dev diagnostics) */
|
|
690
|
+
getRecentEvents(limit) {
|
|
691
|
+
return this.localBus.getRecentEvents(limit);
|
|
692
|
+
}
|
|
693
|
+
/** Clear event log */
|
|
694
|
+
clearEventLog() {
|
|
695
|
+
this.localBus.clearEventLog();
|
|
696
|
+
}
|
|
697
|
+
/** Get listener counts */
|
|
698
|
+
getListenerCounts() {
|
|
699
|
+
return this.localBus.getListenerCounts();
|
|
700
|
+
}
|
|
701
|
+
/** Clear all listeners and log */
|
|
702
|
+
clear() {
|
|
703
|
+
this.localBus.clear();
|
|
704
|
+
}
|
|
705
|
+
/** Disconnect transport */
|
|
706
|
+
async disconnect() {
|
|
707
|
+
await this.transport.close();
|
|
708
|
+
}
|
|
709
|
+
/** Get the instance ID (for debugging) */
|
|
710
|
+
getInstanceId() {
|
|
711
|
+
return this.instanceId;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/lib/eventPersistence.ts
|
|
716
|
+
var InMemoryEventStore = class {
|
|
717
|
+
events = [];
|
|
718
|
+
index = /* @__PURE__ */ new Map();
|
|
719
|
+
async store(event) {
|
|
720
|
+
this.events.push(event);
|
|
721
|
+
this.index.set(event.id, event);
|
|
722
|
+
}
|
|
723
|
+
async query(filters) {
|
|
724
|
+
let results = this.events;
|
|
725
|
+
if (filters.eventName) {
|
|
726
|
+
results = results.filter((e) => e.eventName === filters.eventName);
|
|
727
|
+
}
|
|
728
|
+
if (filters.source) {
|
|
729
|
+
results = results.filter((e) => e.source === filters.source);
|
|
730
|
+
}
|
|
731
|
+
if (filters.traceId) {
|
|
732
|
+
results = results.filter((e) => e.traceId === filters.traceId);
|
|
733
|
+
}
|
|
734
|
+
if (filters.after !== void 0) {
|
|
735
|
+
results = results.filter((e) => e.timestamp > filters.after);
|
|
736
|
+
}
|
|
737
|
+
if (filters.before !== void 0) {
|
|
738
|
+
results = results.filter((e) => e.timestamp < filters.before);
|
|
739
|
+
}
|
|
740
|
+
if (filters.order === "desc") {
|
|
741
|
+
results = [...results].reverse();
|
|
742
|
+
}
|
|
743
|
+
if (filters.limit !== void 0 && filters.limit > 0) {
|
|
744
|
+
results = results.slice(0, filters.limit);
|
|
745
|
+
}
|
|
746
|
+
return results;
|
|
747
|
+
}
|
|
748
|
+
async get(id) {
|
|
749
|
+
return this.index.get(id) ?? null;
|
|
750
|
+
}
|
|
751
|
+
async deleteOlderThan(timestamp) {
|
|
752
|
+
const before = this.events.length;
|
|
753
|
+
this.events = this.events.filter((e) => e.timestamp >= timestamp);
|
|
754
|
+
this.index.clear();
|
|
755
|
+
for (const event of this.events) {
|
|
756
|
+
this.index.set(event.id, event);
|
|
757
|
+
}
|
|
758
|
+
return before - this.events.length;
|
|
759
|
+
}
|
|
760
|
+
async count() {
|
|
761
|
+
return this.events.length;
|
|
762
|
+
}
|
|
763
|
+
async clear() {
|
|
764
|
+
this.events = [];
|
|
765
|
+
this.index.clear();
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
var idCounter = 0;
|
|
769
|
+
var EventPersistence = class {
|
|
770
|
+
store;
|
|
771
|
+
options;
|
|
772
|
+
cleanupTimer = null;
|
|
773
|
+
constructor(options, store) {
|
|
774
|
+
this.options = {
|
|
775
|
+
enabled: options?.enabled ?? true,
|
|
776
|
+
retentionMs: options?.retentionMs ?? 24 * 60 * 60 * 1e3,
|
|
777
|
+
// 24 hours
|
|
778
|
+
maxEvents: options?.maxEvents ?? 1e4,
|
|
779
|
+
cleanupIntervalMs: options?.cleanupIntervalMs ?? 5 * 60 * 1e3,
|
|
780
|
+
// 5 minutes
|
|
781
|
+
defaultSource: options?.defaultSource ?? "unknown"
|
|
782
|
+
};
|
|
783
|
+
this.store = store ?? new InMemoryEventStore();
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Persist an event.
|
|
787
|
+
*/
|
|
788
|
+
async persist(eventName, payload, meta) {
|
|
789
|
+
const event = {
|
|
790
|
+
id: `evt_${++idCounter}_${Date.now()}`,
|
|
791
|
+
eventName,
|
|
792
|
+
payload,
|
|
793
|
+
source: meta?.source ?? this.options.defaultSource,
|
|
794
|
+
timestamp: Date.now(),
|
|
795
|
+
traceId: meta?.traceId ?? `trace_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
796
|
+
meta: meta ? { ...meta } : void 0
|
|
797
|
+
};
|
|
798
|
+
if (this.options.enabled) {
|
|
799
|
+
await this.store.store(event);
|
|
800
|
+
}
|
|
801
|
+
return event;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Replay events matching the query filters.
|
|
805
|
+
*/
|
|
806
|
+
async replay(query) {
|
|
807
|
+
return this.store.query(query);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Get a single event by ID.
|
|
811
|
+
*/
|
|
812
|
+
async getEvent(id) {
|
|
813
|
+
return this.store.get(id);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Get event count.
|
|
817
|
+
*/
|
|
818
|
+
async getEventCount() {
|
|
819
|
+
return this.store.count();
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Run cleanup — delete events older than retention period.
|
|
823
|
+
*/
|
|
824
|
+
async cleanup() {
|
|
825
|
+
const cutoff = Date.now() - this.options.retentionMs;
|
|
826
|
+
return this.store.deleteOlderThan(cutoff);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Start periodic cleanup timer.
|
|
830
|
+
*/
|
|
831
|
+
startCleanup() {
|
|
832
|
+
if (this.cleanupTimer) return;
|
|
833
|
+
this.cleanupTimer = setInterval(() => {
|
|
834
|
+
this.cleanup().catch((err) => {
|
|
835
|
+
console.error("[EventPersistence] Cleanup error:", err);
|
|
836
|
+
});
|
|
837
|
+
}, this.options.cleanupIntervalMs);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Stop periodic cleanup timer.
|
|
841
|
+
*/
|
|
842
|
+
stopCleanup() {
|
|
843
|
+
if (this.cleanupTimer) {
|
|
844
|
+
clearInterval(this.cleanupTimer);
|
|
845
|
+
this.cleanupTimer = null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Clear all persisted events.
|
|
850
|
+
*/
|
|
851
|
+
async clear() {
|
|
852
|
+
await this.store.clear();
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Get the underlying store (for testing or custom queries).
|
|
856
|
+
*/
|
|
857
|
+
getStore() {
|
|
858
|
+
return this.store;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/services/DataService.ts
|
|
863
|
+
init_db();
|
|
864
|
+
var MockDataService = class {
|
|
865
|
+
stores = /* @__PURE__ */ new Map();
|
|
866
|
+
schemas = /* @__PURE__ */ new Map();
|
|
867
|
+
idCounters = /* @__PURE__ */ new Map();
|
|
868
|
+
constructor() {
|
|
869
|
+
if (env.MOCK_SEED !== void 0) {
|
|
870
|
+
faker.seed(env.MOCK_SEED);
|
|
871
|
+
logger.info(`[Mock] Using seed: ${env.MOCK_SEED}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// ============================================================================
|
|
875
|
+
// Store Management
|
|
876
|
+
// ============================================================================
|
|
877
|
+
/**
|
|
878
|
+
* Initialize store for an entity.
|
|
879
|
+
*/
|
|
880
|
+
getStore(entityName) {
|
|
881
|
+
const normalized = entityName.toLowerCase();
|
|
882
|
+
if (!this.stores.has(normalized)) {
|
|
883
|
+
this.stores.set(normalized, /* @__PURE__ */ new Map());
|
|
884
|
+
this.idCounters.set(normalized, 0);
|
|
885
|
+
}
|
|
886
|
+
return this.stores.get(normalized);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Generate next ID for an entity.
|
|
890
|
+
*/
|
|
891
|
+
nextId(entityName) {
|
|
892
|
+
const normalized = entityName.toLowerCase();
|
|
893
|
+
const counter = (this.idCounters.get(normalized) ?? 0) + 1;
|
|
894
|
+
this.idCounters.set(normalized, counter);
|
|
895
|
+
return `mock-${normalized}-${counter}`;
|
|
896
|
+
}
|
|
897
|
+
// ============================================================================
|
|
898
|
+
// Schema & Seeding
|
|
899
|
+
// ============================================================================
|
|
900
|
+
/**
|
|
901
|
+
* Register an entity schema.
|
|
902
|
+
*/
|
|
903
|
+
registerSchema(entityName, schema) {
|
|
904
|
+
this.schemas.set(entityName.toLowerCase(), schema);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Seed an entity with mock data.
|
|
908
|
+
*/
|
|
909
|
+
seed(entityName, fields, count = 10) {
|
|
910
|
+
const store = this.getStore(entityName);
|
|
911
|
+
const normalized = entityName.toLowerCase();
|
|
912
|
+
logger.info(`[Mock] Seeding ${count} ${entityName}...`);
|
|
913
|
+
for (let i = 0; i < count; i++) {
|
|
914
|
+
const item = this.generateMockItem(normalized, fields, i + 1);
|
|
915
|
+
store.set(item.id, item);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Generate a single mock item based on field schemas.
|
|
920
|
+
*/
|
|
921
|
+
generateMockItem(entityName, fields, index) {
|
|
922
|
+
const id = this.nextId(entityName);
|
|
923
|
+
const now = /* @__PURE__ */ new Date();
|
|
924
|
+
const item = {
|
|
925
|
+
id,
|
|
926
|
+
createdAt: faker.date.past({ years: 1 }),
|
|
927
|
+
updatedAt: now
|
|
928
|
+
};
|
|
929
|
+
for (const field of fields) {
|
|
930
|
+
if (field.name === "id" || field.name === "createdAt" || field.name === "updatedAt") {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
item[field.name] = this.generateFieldValue(entityName, field, index);
|
|
934
|
+
}
|
|
935
|
+
return item;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Generate a mock value for a field based on its schema.
|
|
939
|
+
*/
|
|
940
|
+
generateFieldValue(entityName, field, index) {
|
|
941
|
+
if (!field.required && Math.random() > 0.8) {
|
|
942
|
+
return void 0;
|
|
943
|
+
}
|
|
944
|
+
switch (field.type) {
|
|
945
|
+
case "string":
|
|
946
|
+
return this.generateStringValue(entityName, field, index);
|
|
947
|
+
case "number":
|
|
948
|
+
return faker.number.int({
|
|
949
|
+
min: field.min ?? 0,
|
|
950
|
+
max: field.max ?? 1e3
|
|
951
|
+
});
|
|
952
|
+
case "boolean":
|
|
953
|
+
return faker.datatype.boolean();
|
|
954
|
+
case "date":
|
|
955
|
+
return this.generateDateValue(field);
|
|
956
|
+
case "enum":
|
|
957
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
958
|
+
return faker.helpers.arrayElement(field.enumValues);
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
case "relation":
|
|
962
|
+
if (field.relatedEntity) {
|
|
963
|
+
const relatedStore = this.stores.get(field.relatedEntity.toLowerCase());
|
|
964
|
+
if (relatedStore && relatedStore.size > 0) {
|
|
965
|
+
const ids = Array.from(relatedStore.keys());
|
|
966
|
+
return faker.helpers.arrayElement(ids);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
case "array":
|
|
971
|
+
return [];
|
|
972
|
+
default:
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Generate a string value based on field name heuristics.
|
|
978
|
+
* Generic name/title fields use a clean readable format (e.g., "Title 1").
|
|
979
|
+
* Specific fields (email, phone, etc.) use faker.
|
|
980
|
+
*/
|
|
981
|
+
generateStringValue(_entityName, field, index) {
|
|
982
|
+
const name = field.name.toLowerCase();
|
|
983
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
984
|
+
return faker.helpers.arrayElement(field.enumValues);
|
|
985
|
+
}
|
|
986
|
+
if (name.includes("email")) return faker.internet.email();
|
|
987
|
+
if (name === "name" || name === "fullname" || name === "full_name") return faker.person.fullName();
|
|
988
|
+
if (name === "firstname" || name === "first_name") return faker.person.firstName();
|
|
989
|
+
if (name === "lastname" || name === "last_name") return faker.person.lastName();
|
|
990
|
+
if (name.includes("username") || name === "handle") return faker.internet.username();
|
|
991
|
+
if (name.includes("phone") || name.includes("mobile") || name.includes("tel")) return faker.phone.number();
|
|
992
|
+
if (name.includes("address") || name.includes("street")) return faker.location.streetAddress();
|
|
993
|
+
if (name.includes("city")) return faker.location.city();
|
|
994
|
+
if (name.includes("state") || name.includes("province")) return faker.location.state();
|
|
995
|
+
if (name.includes("country")) return faker.location.country();
|
|
996
|
+
if (name.includes("zip") || name.includes("postal")) return faker.location.zipCode();
|
|
997
|
+
if (name === "title" || name === "headline" || name === "subject") return faker.lorem.sentence({ min: 3, max: 7 }).replace(/\.$/, "");
|
|
998
|
+
if (name === "description" || name === "summary" || name === "bio" || name === "about") return faker.lorem.paragraph(2);
|
|
999
|
+
if (name === "content" || name === "body" || name === "text") return faker.lorem.paragraphs(2);
|
|
1000
|
+
if (name === "excerpt" || name === "snippet" || name === "preview") return faker.lorem.sentence({ min: 8, max: 15 });
|
|
1001
|
+
if (name === "label" || name === "tag" || name === "category") return faker.word.noun();
|
|
1002
|
+
if (name === "note" || name === "comment" || name === "message" || name === "feedback") return faker.lorem.sentence();
|
|
1003
|
+
if (name === "status") return faker.helpers.arrayElement(["active", "pending", "completed", "draft", "archived"]);
|
|
1004
|
+
if (name === "priority") return faker.helpers.arrayElement(["low", "medium", "high", "critical"]);
|
|
1005
|
+
if (name === "type" || name === "kind") return faker.helpers.arrayElement(["standard", "premium", "basic", "custom"]);
|
|
1006
|
+
if (name === "role") return faker.helpers.arrayElement(["admin", "editor", "viewer", "member"]);
|
|
1007
|
+
if (name === "level" || name === "tier") return faker.helpers.arrayElement(["beginner", "intermediate", "advanced", "expert"]);
|
|
1008
|
+
if (name === "severity") return faker.helpers.arrayElement(["info", "warning", "error", "critical"]);
|
|
1009
|
+
if (name === "difficulty") return faker.helpers.arrayElement(["easy", "medium", "hard"]);
|
|
1010
|
+
if (name.includes("url") || name.includes("website") || name.includes("link")) return faker.internet.url();
|
|
1011
|
+
if (name.includes("avatar") || name.includes("image") || name.includes("photo") || name.includes("thumbnail")) return faker.image.avatar();
|
|
1012
|
+
if (name.includes("color") || name.includes("colour")) return faker.color.human();
|
|
1013
|
+
if (name.includes("uuid") || name === "guid") return faker.string.uuid();
|
|
1014
|
+
if (name.includes("icon")) return faker.helpers.arrayElement(["star", "heart", "check", "alert-circle", "info", "folder", "file", "user"]);
|
|
1015
|
+
if (name === "slug") return faker.helpers.slugify(faker.lorem.words(3));
|
|
1016
|
+
if (name === "company" || name === "organization" || name === "org") return faker.company.name();
|
|
1017
|
+
if (name === "department") return faker.commerce.department();
|
|
1018
|
+
if (name === "product" || name === "productname" || name === "product_name") return faker.commerce.productName();
|
|
1019
|
+
if (name === "brand") return faker.company.name();
|
|
1020
|
+
if (name === "sku" || name === "code") return faker.string.alphanumeric(8).toUpperCase();
|
|
1021
|
+
if (name === "currency") return faker.finance.currencyCode();
|
|
1022
|
+
if (name.includes("price") || name.includes("cost") || name.includes("amount")) return faker.commerce.price();
|
|
1023
|
+
if (name === "latitude" || name === "lat") return String(faker.location.latitude());
|
|
1024
|
+
if (name === "longitude" || name === "lng" || name === "lon") return String(faker.location.longitude());
|
|
1025
|
+
if (name === "location" || name === "place" || name === "venue") return `${faker.location.city()}, ${faker.location.country()}`;
|
|
1026
|
+
if (name === "region" || name === "zone" || name === "area") return faker.location.state();
|
|
1027
|
+
if (name === "ip" || name.includes("ipaddress") || name === "ip_address") return faker.internet.ip();
|
|
1028
|
+
if (name === "useragent" || name === "user_agent") return faker.internet.userAgent();
|
|
1029
|
+
if (name === "version" || name === "firmware") return faker.system.semver();
|
|
1030
|
+
if (name === "platform" || name === "os") return faker.helpers.arrayElement(["iOS", "Android", "Windows", "macOS", "Linux"]);
|
|
1031
|
+
if (name === "browser") return faker.helpers.arrayElement(["Chrome", "Firefox", "Safari", "Edge"]);
|
|
1032
|
+
if (name === "unit") return faker.helpers.arrayElement(["kg", "lb", "cm", "m", "L", "mL", "\xB0C", "\xB0F", "psi", "rpm"]);
|
|
1033
|
+
if (name === "metric") return faker.helpers.arrayElement(["temperature", "pressure", "humidity", "speed", "voltage"]);
|
|
1034
|
+
if (name === "operator") return faker.helpers.arrayElement(["gt", "lt", "eq", "gte", "lte"]);
|
|
1035
|
+
if (name === "format" || name === "mimetype" || name === "mime_type") return faker.system.mimeType();
|
|
1036
|
+
if (name === "extension" || name === "ext") return faker.system.fileExt();
|
|
1037
|
+
if (name === "filename" || name === "file_name") return faker.system.fileName();
|
|
1038
|
+
if (name.endsWith("id") || name.endsWith("_id")) return faker.string.alphanumeric(8).toUpperCase();
|
|
1039
|
+
if (name.endsWith("name") || name.endsWith("_name")) return faker.person.fullName();
|
|
1040
|
+
if (name.endsWith("type") || name.endsWith("_type")) return faker.helpers.arrayElement(["standard", "premium", "basic", "custom", "special"]);
|
|
1041
|
+
if (name.endsWith("date") || name.endsWith("_date") || name.endsWith("at")) return faker.date.recent({ days: 90 }).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
1042
|
+
if (name.endsWith("count") || name.endsWith("_count")) return String(faker.number.int({ min: 1, max: 100 }));
|
|
1043
|
+
if (name.includes("reason") || name.includes("detail")) return faker.lorem.sentence({ min: 4, max: 8 }).replace(/\.$/, "");
|
|
1044
|
+
if (name.includes("field") || name.includes("key") || name.includes("attribute")) return faker.word.noun();
|
|
1045
|
+
if (name.includes("value") || name.includes("result") || name.includes("output")) return faker.word.words({ count: { min: 1, max: 3 } });
|
|
1046
|
+
if (name.includes("direction") || name.includes("position") || name.includes("mode")) return faker.helpers.arrayElement(["left", "right", "center", "top", "bottom", "auto"]);
|
|
1047
|
+
return faker.word.words({ count: { min: 1, max: 3 } });
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Capitalize first letter of a string.
|
|
1051
|
+
*/
|
|
1052
|
+
capitalizeFirst(str) {
|
|
1053
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Generate a date value based on field name heuristics.
|
|
1057
|
+
*/
|
|
1058
|
+
generateDateValue(field) {
|
|
1059
|
+
const name = field.name.toLowerCase();
|
|
1060
|
+
if (name.includes("created") || name.includes("start") || name.includes("birth")) {
|
|
1061
|
+
return faker.date.past({ years: 2 });
|
|
1062
|
+
}
|
|
1063
|
+
if (name.includes("updated") || name.includes("modified")) {
|
|
1064
|
+
return faker.date.recent({ days: 30 });
|
|
1065
|
+
}
|
|
1066
|
+
if (name.includes("deadline") || name.includes("due") || name.includes("end") || name.includes("expires")) {
|
|
1067
|
+
return faker.date.future({ years: 1 });
|
|
1068
|
+
}
|
|
1069
|
+
return faker.date.anytime();
|
|
1070
|
+
}
|
|
1071
|
+
// ============================================================================
|
|
1072
|
+
// CRUD Operations
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
/**
|
|
1075
|
+
* List all items of an entity.
|
|
1076
|
+
*/
|
|
1077
|
+
list(entityName) {
|
|
1078
|
+
const store = this.getStore(entityName);
|
|
1079
|
+
return Array.from(store.values());
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Get a single item by ID.
|
|
1083
|
+
*/
|
|
1084
|
+
getById(entityName, id) {
|
|
1085
|
+
const store = this.getStore(entityName);
|
|
1086
|
+
const item = store.get(id);
|
|
1087
|
+
return item ?? null;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Create a new item.
|
|
1091
|
+
*/
|
|
1092
|
+
create(entityName, data) {
|
|
1093
|
+
const store = this.getStore(entityName);
|
|
1094
|
+
const id = this.nextId(entityName);
|
|
1095
|
+
const now = /* @__PURE__ */ new Date();
|
|
1096
|
+
const item = {
|
|
1097
|
+
...data,
|
|
1098
|
+
id,
|
|
1099
|
+
createdAt: now,
|
|
1100
|
+
updatedAt: now
|
|
1101
|
+
};
|
|
1102
|
+
store.set(id, item);
|
|
1103
|
+
return item;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Update an existing item.
|
|
1107
|
+
*/
|
|
1108
|
+
update(entityName, id, data) {
|
|
1109
|
+
const store = this.getStore(entityName);
|
|
1110
|
+
const existing = store.get(id);
|
|
1111
|
+
if (!existing) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
const updated = {
|
|
1115
|
+
...existing,
|
|
1116
|
+
...data,
|
|
1117
|
+
id,
|
|
1118
|
+
// Preserve original ID
|
|
1119
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1120
|
+
};
|
|
1121
|
+
store.set(id, updated);
|
|
1122
|
+
return updated;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Delete an item.
|
|
1126
|
+
*/
|
|
1127
|
+
delete(entityName, id) {
|
|
1128
|
+
const store = this.getStore(entityName);
|
|
1129
|
+
if (!store.has(id)) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
store.delete(id);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
// ============================================================================
|
|
1136
|
+
// Utilities
|
|
1137
|
+
// ============================================================================
|
|
1138
|
+
/**
|
|
1139
|
+
* Clear all data for an entity.
|
|
1140
|
+
*/
|
|
1141
|
+
clear(entityName) {
|
|
1142
|
+
const normalized = entityName.toLowerCase();
|
|
1143
|
+
this.stores.delete(normalized);
|
|
1144
|
+
this.idCounters.delete(normalized);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Clear all data.
|
|
1148
|
+
*/
|
|
1149
|
+
clearAll() {
|
|
1150
|
+
this.stores.clear();
|
|
1151
|
+
this.idCounters.clear();
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Get count of items for an entity.
|
|
1155
|
+
*/
|
|
1156
|
+
count(entityName) {
|
|
1157
|
+
const store = this.getStore(entityName);
|
|
1158
|
+
return store.size;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
var _mockDataService = null;
|
|
1162
|
+
function getMockDataService() {
|
|
1163
|
+
if (!_mockDataService) {
|
|
1164
|
+
_mockDataService = new MockDataService();
|
|
1165
|
+
}
|
|
1166
|
+
return _mockDataService;
|
|
1167
|
+
}
|
|
1168
|
+
function resetMockDataService() {
|
|
1169
|
+
_mockDataService?.clearAll();
|
|
1170
|
+
_mockDataService = null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/utils/queryFilters.ts
|
|
1174
|
+
var OPERATOR_MAP = {
|
|
1175
|
+
"eq": "==",
|
|
1176
|
+
"neq": "!=",
|
|
1177
|
+
"gt": ">",
|
|
1178
|
+
"gte": ">=",
|
|
1179
|
+
"lt": "<",
|
|
1180
|
+
"lte": "<=",
|
|
1181
|
+
"contains": "array-contains",
|
|
1182
|
+
"contains_any": "array-contains-any",
|
|
1183
|
+
"in": "in",
|
|
1184
|
+
"not_in": "not-in",
|
|
1185
|
+
// Date operators map to same comparison operators
|
|
1186
|
+
"date_eq": "==",
|
|
1187
|
+
"date_gte": ">=",
|
|
1188
|
+
"date_lte": "<="
|
|
1189
|
+
};
|
|
1190
|
+
var RESERVED_PARAMS = /* @__PURE__ */ new Set([
|
|
1191
|
+
"page",
|
|
1192
|
+
"pageSize",
|
|
1193
|
+
"limit",
|
|
1194
|
+
"offset",
|
|
1195
|
+
"search",
|
|
1196
|
+
"q",
|
|
1197
|
+
"sortBy",
|
|
1198
|
+
"sortOrder",
|
|
1199
|
+
"orderBy",
|
|
1200
|
+
"orderDirection"
|
|
1201
|
+
]);
|
|
1202
|
+
function parseQueryFilters(query) {
|
|
1203
|
+
const filters = [];
|
|
1204
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1205
|
+
if (RESERVED_PARAMS.has(key)) continue;
|
|
1206
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
1207
|
+
const match = key.match(/^(.+)__(\w+)$/);
|
|
1208
|
+
if (match) {
|
|
1209
|
+
const [, field, op] = match;
|
|
1210
|
+
const firestoreOp = OPERATOR_MAP[op];
|
|
1211
|
+
if (firestoreOp) {
|
|
1212
|
+
filters.push({
|
|
1213
|
+
field,
|
|
1214
|
+
operator: firestoreOp,
|
|
1215
|
+
value: parseValue(value, op)
|
|
1216
|
+
});
|
|
1217
|
+
} else {
|
|
1218
|
+
filters.push({
|
|
1219
|
+
field: key,
|
|
1220
|
+
operator: "==",
|
|
1221
|
+
value: parseValue(value, "eq")
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
1225
|
+
filters.push({
|
|
1226
|
+
field: key,
|
|
1227
|
+
operator: "==",
|
|
1228
|
+
value: parseValue(value, "eq")
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return filters;
|
|
1233
|
+
}
|
|
1234
|
+
function parseValue(value, operator) {
|
|
1235
|
+
if (operator === "in" || operator === "not_in" || operator === "contains_any") {
|
|
1236
|
+
if (typeof value === "string") {
|
|
1237
|
+
return value.split(",").map((v) => v.trim());
|
|
1238
|
+
}
|
|
1239
|
+
if (Array.isArray(value)) {
|
|
1240
|
+
return value;
|
|
1241
|
+
}
|
|
1242
|
+
return [value];
|
|
1243
|
+
}
|
|
1244
|
+
if (typeof value === "string") {
|
|
1245
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
1246
|
+
const num = parseFloat(value);
|
|
1247
|
+
if (!isNaN(num)) {
|
|
1248
|
+
return num;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (value === "true") return true;
|
|
1252
|
+
if (value === "false") return false;
|
|
1253
|
+
}
|
|
1254
|
+
return value;
|
|
1255
|
+
}
|
|
1256
|
+
function applyFiltersToQuery(collection, filters) {
|
|
1257
|
+
let query = collection;
|
|
1258
|
+
for (const filter of filters) {
|
|
1259
|
+
query = query.where(
|
|
1260
|
+
filter.field,
|
|
1261
|
+
filter.operator,
|
|
1262
|
+
filter.value
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
return query;
|
|
1266
|
+
}
|
|
1267
|
+
function extractPaginationParams(query, defaults = {}) {
|
|
1268
|
+
return {
|
|
1269
|
+
page: parseInt(query.page, 10) || defaults.page || 1,
|
|
1270
|
+
pageSize: parseInt(query.pageSize, 10) || parseInt(query.limit, 10) || defaults.pageSize || 20,
|
|
1271
|
+
sortBy: query.sortBy || query.orderBy,
|
|
1272
|
+
sortOrder: query.sortOrder || query.orderDirection || defaults.sortOrder || "asc"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/services/DataService.ts
|
|
1277
|
+
function applyFilterCondition(value, operator, filterValue) {
|
|
1278
|
+
if (value === null || value === void 0) {
|
|
1279
|
+
return operator === "!=" ? filterValue !== null : false;
|
|
1280
|
+
}
|
|
1281
|
+
switch (operator) {
|
|
1282
|
+
case "==":
|
|
1283
|
+
return value === filterValue;
|
|
1284
|
+
case "!=":
|
|
1285
|
+
return value !== filterValue;
|
|
1286
|
+
case ">":
|
|
1287
|
+
return value > filterValue;
|
|
1288
|
+
case ">=":
|
|
1289
|
+
return value >= filterValue;
|
|
1290
|
+
case "<":
|
|
1291
|
+
return value < filterValue;
|
|
1292
|
+
case "<=":
|
|
1293
|
+
return value <= filterValue;
|
|
1294
|
+
case "array-contains":
|
|
1295
|
+
return Array.isArray(value) && value.includes(filterValue);
|
|
1296
|
+
case "array-contains-any":
|
|
1297
|
+
return Array.isArray(value) && Array.isArray(filterValue) && filterValue.some((v) => value.includes(v));
|
|
1298
|
+
case "in":
|
|
1299
|
+
return Array.isArray(filterValue) && filterValue.includes(value);
|
|
1300
|
+
case "not-in":
|
|
1301
|
+
return Array.isArray(filterValue) && !filterValue.includes(value);
|
|
1302
|
+
default:
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
var MockDataServiceAdapter = class {
|
|
1307
|
+
async list(collection) {
|
|
1308
|
+
return getMockDataService().list(collection);
|
|
1309
|
+
}
|
|
1310
|
+
async listPaginated(collection, options = {}) {
|
|
1311
|
+
const {
|
|
1312
|
+
page = 1,
|
|
1313
|
+
pageSize = 20,
|
|
1314
|
+
search,
|
|
1315
|
+
searchFields,
|
|
1316
|
+
sortBy,
|
|
1317
|
+
sortOrder = "asc",
|
|
1318
|
+
filters
|
|
1319
|
+
} = options;
|
|
1320
|
+
let items = getMockDataService().list(collection);
|
|
1321
|
+
if (filters && filters.length > 0) {
|
|
1322
|
+
items = items.filter((item) => {
|
|
1323
|
+
const record = item;
|
|
1324
|
+
return filters.every((filter) => {
|
|
1325
|
+
const value = record[filter.field];
|
|
1326
|
+
return applyFilterCondition(value, filter.operator, filter.value);
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (search && search.trim()) {
|
|
1331
|
+
const searchLower = search.toLowerCase();
|
|
1332
|
+
items = items.filter((item) => {
|
|
1333
|
+
const record = item;
|
|
1334
|
+
const fieldsToSearch = searchFields || Object.keys(record);
|
|
1335
|
+
return fieldsToSearch.some((field) => {
|
|
1336
|
+
const value = record[field];
|
|
1337
|
+
if (value === null || value === void 0) return false;
|
|
1338
|
+
return String(value).toLowerCase().includes(searchLower);
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (sortBy) {
|
|
1343
|
+
items = [...items].sort((a, b) => {
|
|
1344
|
+
const aVal = a[sortBy];
|
|
1345
|
+
const bVal = b[sortBy];
|
|
1346
|
+
if (aVal === bVal) return 0;
|
|
1347
|
+
if (aVal === null || aVal === void 0) return 1;
|
|
1348
|
+
if (bVal === null || bVal === void 0) return -1;
|
|
1349
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
1350
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
const total = items.length;
|
|
1354
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
1355
|
+
const startIndex = (page - 1) * pageSize;
|
|
1356
|
+
const data = items.slice(startIndex, startIndex + pageSize);
|
|
1357
|
+
return { data, total, page, pageSize, totalPages };
|
|
1358
|
+
}
|
|
1359
|
+
async getById(collection, id) {
|
|
1360
|
+
return getMockDataService().getById(collection, id);
|
|
1361
|
+
}
|
|
1362
|
+
async create(collection, data) {
|
|
1363
|
+
return getMockDataService().create(collection, data);
|
|
1364
|
+
}
|
|
1365
|
+
async update(collection, id, data) {
|
|
1366
|
+
return getMockDataService().update(collection, id, data);
|
|
1367
|
+
}
|
|
1368
|
+
async delete(collection, id) {
|
|
1369
|
+
return getMockDataService().delete(collection, id);
|
|
1370
|
+
}
|
|
1371
|
+
async query(collection, filters) {
|
|
1372
|
+
let items = getMockDataService().list(collection);
|
|
1373
|
+
for (const filter of filters) {
|
|
1374
|
+
items = items.filter((item) => {
|
|
1375
|
+
const value = item[filter.field];
|
|
1376
|
+
return applyFilterCondition(value, filter.op, filter.value);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
return items;
|
|
1380
|
+
}
|
|
1381
|
+
getStore(collection) {
|
|
1382
|
+
const adapter = this;
|
|
1383
|
+
return {
|
|
1384
|
+
async getById(id) {
|
|
1385
|
+
return adapter.getById(collection, id);
|
|
1386
|
+
},
|
|
1387
|
+
async create(data) {
|
|
1388
|
+
return adapter.create(collection, data);
|
|
1389
|
+
},
|
|
1390
|
+
async update(id, data) {
|
|
1391
|
+
const result = await adapter.update(collection, id, data);
|
|
1392
|
+
if (!result) throw new Error(`Entity ${id} not found in ${collection}`);
|
|
1393
|
+
return result;
|
|
1394
|
+
},
|
|
1395
|
+
async delete(id) {
|
|
1396
|
+
adapter.delete(collection, id);
|
|
1397
|
+
},
|
|
1398
|
+
async query(filters) {
|
|
1399
|
+
return adapter.query(collection, filters);
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
var FirebaseDataService = class {
|
|
1405
|
+
async list(collection) {
|
|
1406
|
+
const snapshot = await db.collection(collection).get();
|
|
1407
|
+
return snapshot.docs.map((doc) => ({
|
|
1408
|
+
id: doc.id,
|
|
1409
|
+
...doc.data()
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
async listPaginated(collection, options = {}) {
|
|
1413
|
+
const {
|
|
1414
|
+
page = 1,
|
|
1415
|
+
pageSize = 20,
|
|
1416
|
+
search,
|
|
1417
|
+
searchFields,
|
|
1418
|
+
sortBy,
|
|
1419
|
+
sortOrder = "asc",
|
|
1420
|
+
filters
|
|
1421
|
+
} = options;
|
|
1422
|
+
let query = db.collection(collection);
|
|
1423
|
+
if (filters && filters.length > 0) {
|
|
1424
|
+
query = applyFiltersToQuery(query, filters);
|
|
1425
|
+
}
|
|
1426
|
+
if (sortBy && !search) {
|
|
1427
|
+
query = query.orderBy(sortBy, sortOrder);
|
|
1428
|
+
}
|
|
1429
|
+
const snapshot = await query.get();
|
|
1430
|
+
let items = snapshot.docs.map((doc) => ({
|
|
1431
|
+
id: doc.id,
|
|
1432
|
+
...doc.data()
|
|
1433
|
+
}));
|
|
1434
|
+
if (search && search.trim()) {
|
|
1435
|
+
const searchLower = search.toLowerCase();
|
|
1436
|
+
items = items.filter((item) => {
|
|
1437
|
+
const record = item;
|
|
1438
|
+
const fieldsToSearch = searchFields || Object.keys(record);
|
|
1439
|
+
return fieldsToSearch.some((field) => {
|
|
1440
|
+
const value = record[field];
|
|
1441
|
+
if (value === null || value === void 0) return false;
|
|
1442
|
+
return String(value).toLowerCase().includes(searchLower);
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
if (sortBy && search) {
|
|
1447
|
+
items = [...items].sort((a, b) => {
|
|
1448
|
+
const aVal = a[sortBy];
|
|
1449
|
+
const bVal = b[sortBy];
|
|
1450
|
+
if (aVal === bVal) return 0;
|
|
1451
|
+
if (aVal === null || aVal === void 0) return 1;
|
|
1452
|
+
if (bVal === null || bVal === void 0) return -1;
|
|
1453
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
1454
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
const total = items.length;
|
|
1458
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
1459
|
+
const startIndex = (page - 1) * pageSize;
|
|
1460
|
+
const data = items.slice(startIndex, startIndex + pageSize);
|
|
1461
|
+
return { data, total, page, pageSize, totalPages };
|
|
1462
|
+
}
|
|
1463
|
+
async getById(collection, id) {
|
|
1464
|
+
const doc = await db.collection(collection).doc(id).get();
|
|
1465
|
+
if (!doc.exists) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
return { id: doc.id, ...doc.data() };
|
|
1469
|
+
}
|
|
1470
|
+
async create(collection, data) {
|
|
1471
|
+
const now = /* @__PURE__ */ new Date();
|
|
1472
|
+
const docRef = await db.collection(collection).add({
|
|
1473
|
+
...data,
|
|
1474
|
+
createdAt: now,
|
|
1475
|
+
updatedAt: now
|
|
1476
|
+
});
|
|
1477
|
+
return {
|
|
1478
|
+
...data,
|
|
1479
|
+
id: docRef.id,
|
|
1480
|
+
createdAt: now,
|
|
1481
|
+
updatedAt: now
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
async update(collection, id, data) {
|
|
1485
|
+
const docRef = db.collection(collection).doc(id);
|
|
1486
|
+
const doc = await docRef.get();
|
|
1487
|
+
if (!doc.exists) {
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
const now = /* @__PURE__ */ new Date();
|
|
1491
|
+
await docRef.update({
|
|
1492
|
+
...data,
|
|
1493
|
+
updatedAt: now
|
|
1494
|
+
});
|
|
1495
|
+
return {
|
|
1496
|
+
...doc.data(),
|
|
1497
|
+
...data,
|
|
1498
|
+
id,
|
|
1499
|
+
updatedAt: now
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
async delete(collection, id) {
|
|
1503
|
+
const docRef = db.collection(collection).doc(id);
|
|
1504
|
+
const doc = await docRef.get();
|
|
1505
|
+
if (!doc.exists) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
await docRef.delete();
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
async query(collection, filters) {
|
|
1512
|
+
let query = db.collection(collection);
|
|
1513
|
+
const memoryFilters = [];
|
|
1514
|
+
for (const filter of filters) {
|
|
1515
|
+
if (["==", "!=", "<", "<=", ">", ">=", "in", "not-in"].includes(filter.op)) {
|
|
1516
|
+
query = query.where(filter.field, filter.op, filter.value);
|
|
1517
|
+
} else {
|
|
1518
|
+
memoryFilters.push(filter);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const snapshot = await query.get();
|
|
1522
|
+
let items = snapshot.docs.map((doc) => ({
|
|
1523
|
+
id: doc.id,
|
|
1524
|
+
...doc.data()
|
|
1525
|
+
}));
|
|
1526
|
+
for (const filter of memoryFilters) {
|
|
1527
|
+
items = items.filter((item) => {
|
|
1528
|
+
const value = item[filter.field];
|
|
1529
|
+
return applyFilterCondition(value, filter.op, filter.value);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
return items;
|
|
1533
|
+
}
|
|
1534
|
+
getStore(collection) {
|
|
1535
|
+
const svc = this;
|
|
1536
|
+
return {
|
|
1537
|
+
async getById(id) {
|
|
1538
|
+
return svc.getById(collection, id);
|
|
1539
|
+
},
|
|
1540
|
+
async create(data) {
|
|
1541
|
+
return svc.create(collection, data);
|
|
1542
|
+
},
|
|
1543
|
+
async update(id, data) {
|
|
1544
|
+
const result = await svc.update(collection, id, data);
|
|
1545
|
+
if (!result) throw new Error(`Entity ${id} not found in ${collection}`);
|
|
1546
|
+
return result;
|
|
1547
|
+
},
|
|
1548
|
+
async delete(id) {
|
|
1549
|
+
await svc.delete(collection, id);
|
|
1550
|
+
},
|
|
1551
|
+
async query(filters) {
|
|
1552
|
+
return svc.query(collection, filters);
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
function createDataService() {
|
|
1558
|
+
if (env.USE_MOCK_DATA) {
|
|
1559
|
+
logger.info("[DataService] Using MockDataService");
|
|
1560
|
+
return new MockDataServiceAdapter();
|
|
1561
|
+
}
|
|
1562
|
+
logger.info("[DataService] Using FirebaseDataService");
|
|
1563
|
+
return new FirebaseDataService();
|
|
1564
|
+
}
|
|
1565
|
+
var _dataService = null;
|
|
1566
|
+
function getDataService() {
|
|
1567
|
+
if (!_dataService) {
|
|
1568
|
+
_dataService = createDataService();
|
|
1569
|
+
}
|
|
1570
|
+
return _dataService;
|
|
1571
|
+
}
|
|
1572
|
+
function resetDataService() {
|
|
1573
|
+
_dataService = null;
|
|
1574
|
+
}
|
|
1575
|
+
function seedMockData(entities) {
|
|
1576
|
+
if (!env.USE_MOCK_DATA) {
|
|
1577
|
+
logger.info("[DataService] Mock mode disabled, skipping seed");
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
logger.info("[DataService] Seeding mock data...");
|
|
1581
|
+
for (const entity of entities) {
|
|
1582
|
+
getMockDataService().seed(entity.name, entity.fields, entity.seedCount);
|
|
1583
|
+
}
|
|
1584
|
+
logger.info("[DataService] Mock data seeding complete");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/lib/debugRouter.ts
|
|
1588
|
+
function debugEventsRouter() {
|
|
1589
|
+
const router2 = Router();
|
|
1590
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1591
|
+
return router2;
|
|
1592
|
+
}
|
|
1593
|
+
router2.get("/event-log", (_req, res) => {
|
|
1594
|
+
const limit = parseInt(String(_req.query.limit) || "50", 10);
|
|
1595
|
+
const events = getServerEventBus().getRecentEvents(limit);
|
|
1596
|
+
res.json({ count: events.length, events });
|
|
1597
|
+
});
|
|
1598
|
+
router2.delete("/event-log", (_req, res) => {
|
|
1599
|
+
getServerEventBus().clearEventLog();
|
|
1600
|
+
res.json({ cleared: true });
|
|
1601
|
+
});
|
|
1602
|
+
router2.get("/listeners", (_req, res) => {
|
|
1603
|
+
const counts = getServerEventBus().getListenerCounts();
|
|
1604
|
+
const total = Object.values(counts).reduce((sum, n) => sum + n, 0);
|
|
1605
|
+
res.json({ total, events: counts });
|
|
1606
|
+
});
|
|
1607
|
+
router2.post("/seed", (req, res) => {
|
|
1608
|
+
const { entities } = req.body;
|
|
1609
|
+
if (!entities || !Array.isArray(entities)) {
|
|
1610
|
+
res.status(400).json({ error: 'Body must have "entities" array' });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const configs = entities.map((e) => ({
|
|
1614
|
+
name: e.name,
|
|
1615
|
+
fields: e.fields,
|
|
1616
|
+
seedCount: e.seedCount ?? 5
|
|
1617
|
+
}));
|
|
1618
|
+
seedMockData(configs);
|
|
1619
|
+
const summary = configs.map((c) => `${c.name}(${c.seedCount})`).join(", ");
|
|
1620
|
+
res.json({ seeded: true, summary });
|
|
1621
|
+
});
|
|
1622
|
+
return router2;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/index.ts
|
|
1626
|
+
init_db();
|
|
1627
|
+
var wss = null;
|
|
1628
|
+
function setupEventBroadcast(server, path = "/ws/events") {
|
|
1629
|
+
if (wss) {
|
|
1630
|
+
logger.warn("[WebSocket] Server already initialized");
|
|
1631
|
+
return wss;
|
|
1632
|
+
}
|
|
1633
|
+
wss = new WebSocketServer({ server, path });
|
|
1634
|
+
logger.info(`[WebSocket] Server listening at ${path}`);
|
|
1635
|
+
wss.on("connection", (ws, req) => {
|
|
1636
|
+
const clientId = req.headers["sec-websocket-key"] || "unknown";
|
|
1637
|
+
logger.debug(`[WebSocket] Client connected: ${clientId}`);
|
|
1638
|
+
ws.send(
|
|
1639
|
+
JSON.stringify({
|
|
1640
|
+
type: "CONNECTED",
|
|
1641
|
+
timestamp: Date.now(),
|
|
1642
|
+
message: "Connected to event stream"
|
|
1643
|
+
})
|
|
1644
|
+
);
|
|
1645
|
+
ws.on("message", (data) => {
|
|
1646
|
+
try {
|
|
1647
|
+
const message = JSON.parse(data.toString());
|
|
1648
|
+
logger.debug(`[WebSocket] Received from ${clientId}:`, message);
|
|
1649
|
+
if (message.type && message.payload) {
|
|
1650
|
+
getServerEventBus().emit(message.type, message.payload, {
|
|
1651
|
+
orbital: "client",
|
|
1652
|
+
entity: clientId
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
logger.error(`[WebSocket] Failed to parse message:`, error);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
ws.on("close", () => {
|
|
1660
|
+
logger.debug(`[WebSocket] Client disconnected: ${clientId}`);
|
|
1661
|
+
});
|
|
1662
|
+
ws.on("error", (error) => {
|
|
1663
|
+
logger.error(`[WebSocket] Client error:`, error);
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
getServerEventBus().on("*", (event) => {
|
|
1667
|
+
if (!wss) return;
|
|
1668
|
+
const typedEvent = event;
|
|
1669
|
+
const message = JSON.stringify({
|
|
1670
|
+
type: typedEvent.type,
|
|
1671
|
+
payload: typedEvent.payload,
|
|
1672
|
+
timestamp: typedEvent.timestamp,
|
|
1673
|
+
source: typedEvent.source
|
|
1674
|
+
});
|
|
1675
|
+
let broadcastCount = 0;
|
|
1676
|
+
wss.clients.forEach((client) => {
|
|
1677
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1678
|
+
client.send(message);
|
|
1679
|
+
broadcastCount++;
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
if (broadcastCount > 0) {
|
|
1683
|
+
logger.debug(`[WebSocket] Broadcast ${typedEvent.type} to ${broadcastCount} client(s)`);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
return wss;
|
|
1687
|
+
}
|
|
1688
|
+
function getWebSocketServer() {
|
|
1689
|
+
return wss;
|
|
1690
|
+
}
|
|
1691
|
+
function closeWebSocketServer() {
|
|
1692
|
+
return new Promise((resolve, reject) => {
|
|
1693
|
+
if (!wss) {
|
|
1694
|
+
resolve();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
wss.close((err) => {
|
|
1698
|
+
if (err) {
|
|
1699
|
+
reject(err);
|
|
1700
|
+
} else {
|
|
1701
|
+
wss = null;
|
|
1702
|
+
resolve();
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
function getConnectedClientCount() {
|
|
1708
|
+
if (!wss) return 0;
|
|
1709
|
+
return wss.clients.size;
|
|
1710
|
+
}
|
|
1711
|
+
var AppError = class extends Error {
|
|
1712
|
+
constructor(statusCode, message, code) {
|
|
1713
|
+
super(message);
|
|
1714
|
+
this.statusCode = statusCode;
|
|
1715
|
+
this.message = message;
|
|
1716
|
+
this.code = code;
|
|
1717
|
+
this.name = "AppError";
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
var NotFoundError = class extends AppError {
|
|
1721
|
+
constructor(message = "Resource not found") {
|
|
1722
|
+
super(404, message, "NOT_FOUND");
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
var ValidationError = class extends AppError {
|
|
1726
|
+
constructor(message = "Validation failed") {
|
|
1727
|
+
super(400, message, "VALIDATION_ERROR");
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
var UnauthorizedError = class extends AppError {
|
|
1731
|
+
constructor(message = "Unauthorized") {
|
|
1732
|
+
super(401, message, "UNAUTHORIZED");
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
var ForbiddenError = class extends AppError {
|
|
1736
|
+
constructor(message = "Forbidden") {
|
|
1737
|
+
super(403, message, "FORBIDDEN");
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
var ConflictError = class extends AppError {
|
|
1741
|
+
constructor(message = "Resource conflict") {
|
|
1742
|
+
super(409, message, "CONFLICT");
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
var errorHandler = (err, _req, res, _next) => {
|
|
1746
|
+
logger.error("Error:", { name: err.name, message: err.message, stack: err.stack });
|
|
1747
|
+
if (err instanceof ZodError) {
|
|
1748
|
+
res.status(400).json({
|
|
1749
|
+
success: false,
|
|
1750
|
+
error: "Validation failed",
|
|
1751
|
+
code: "VALIDATION_ERROR",
|
|
1752
|
+
details: err.errors.map((e) => ({
|
|
1753
|
+
path: e.path.join("."),
|
|
1754
|
+
message: e.message
|
|
1755
|
+
}))
|
|
1756
|
+
});
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (err instanceof AppError) {
|
|
1760
|
+
res.status(err.statusCode).json({
|
|
1761
|
+
success: false,
|
|
1762
|
+
error: err.message,
|
|
1763
|
+
code: err.code
|
|
1764
|
+
});
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (err.name === "FirebaseError" || err.name === "FirestoreError") {
|
|
1768
|
+
res.status(500).json({
|
|
1769
|
+
success: false,
|
|
1770
|
+
error: "Database error",
|
|
1771
|
+
code: "DATABASE_ERROR"
|
|
1772
|
+
});
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
res.status(500).json({
|
|
1776
|
+
success: false,
|
|
1777
|
+
error: "Internal server error",
|
|
1778
|
+
code: "INTERNAL_ERROR"
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
var asyncHandler = (fn) => (req, res, next) => {
|
|
1782
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
1783
|
+
};
|
|
1784
|
+
var notFoundHandler = (req, res) => {
|
|
1785
|
+
res.status(404).json({
|
|
1786
|
+
success: false,
|
|
1787
|
+
error: `Route ${req.method} ${req.path} not found`,
|
|
1788
|
+
code: "ROUTE_NOT_FOUND"
|
|
1789
|
+
});
|
|
1790
|
+
};
|
|
1791
|
+
var validateBody = (schema) => async (req, res, next) => {
|
|
1792
|
+
try {
|
|
1793
|
+
req.body = await schema.parseAsync(req.body);
|
|
1794
|
+
next();
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
if (error instanceof ZodError) {
|
|
1797
|
+
res.status(400).json({
|
|
1798
|
+
success: false,
|
|
1799
|
+
error: "Validation failed",
|
|
1800
|
+
code: "VALIDATION_ERROR",
|
|
1801
|
+
details: error.errors.map((e) => ({
|
|
1802
|
+
path: e.path.join("."),
|
|
1803
|
+
message: e.message
|
|
1804
|
+
}))
|
|
1805
|
+
});
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
next(error);
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
var validateQuery = (schema) => async (req, res, next) => {
|
|
1812
|
+
try {
|
|
1813
|
+
req.query = await schema.parseAsync(req.query);
|
|
1814
|
+
next();
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (error instanceof ZodError) {
|
|
1817
|
+
res.status(400).json({
|
|
1818
|
+
success: false,
|
|
1819
|
+
error: "Invalid query parameters",
|
|
1820
|
+
code: "VALIDATION_ERROR",
|
|
1821
|
+
details: error.errors.map((e) => ({
|
|
1822
|
+
path: e.path.join("."),
|
|
1823
|
+
message: e.message
|
|
1824
|
+
}))
|
|
1825
|
+
});
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
next(error);
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
var validateParams = (schema) => async (req, res, next) => {
|
|
1832
|
+
try {
|
|
1833
|
+
req.params = await schema.parseAsync(req.params);
|
|
1834
|
+
next();
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
if (error instanceof ZodError) {
|
|
1837
|
+
res.status(400).json({
|
|
1838
|
+
success: false,
|
|
1839
|
+
error: "Invalid path parameters",
|
|
1840
|
+
code: "VALIDATION_ERROR",
|
|
1841
|
+
details: error.errors.map((e) => ({
|
|
1842
|
+
path: e.path.join("."),
|
|
1843
|
+
message: e.message
|
|
1844
|
+
}))
|
|
1845
|
+
});
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
next(error);
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// src/middleware/authenticateFirebase.ts
|
|
1853
|
+
init_db();
|
|
1854
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1855
|
+
var DEV_USER = {
|
|
1856
|
+
uid: "dev-user-001",
|
|
1857
|
+
email: "dev@localhost",
|
|
1858
|
+
email_verified: true,
|
|
1859
|
+
aud: "dev-project",
|
|
1860
|
+
auth_time: Math.floor(Date.now() / 1e3),
|
|
1861
|
+
exp: Math.floor(Date.now() / 1e3) + 3600,
|
|
1862
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1863
|
+
iss: "https://securetoken.google.com/dev-project",
|
|
1864
|
+
sub: "dev-user-001",
|
|
1865
|
+
firebase: {
|
|
1866
|
+
identities: {},
|
|
1867
|
+
sign_in_provider: "custom"
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
async function authenticateFirebase(req, res, next) {
|
|
1871
|
+
const authorization = req.headers.authorization;
|
|
1872
|
+
if (env.NODE_ENV === "development" && (!authorization || !authorization.startsWith(BEARER_PREFIX))) {
|
|
1873
|
+
req.firebaseUser = DEV_USER;
|
|
1874
|
+
res.locals.firebaseUser = DEV_USER;
|
|
1875
|
+
return next();
|
|
1876
|
+
}
|
|
1877
|
+
try {
|
|
1878
|
+
if (!authorization || !authorization.startsWith(BEARER_PREFIX)) {
|
|
1879
|
+
return res.status(401).json({ error: "Authorization header missing or malformed" });
|
|
1880
|
+
}
|
|
1881
|
+
const token = authorization.slice(BEARER_PREFIX.length);
|
|
1882
|
+
const decodedToken = await getAuth().verifyIdToken(token);
|
|
1883
|
+
req.firebaseUser = decodedToken;
|
|
1884
|
+
res.locals.firebaseUser = decodedToken;
|
|
1885
|
+
return next();
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
console.error("Firebase authentication failed:", error);
|
|
1888
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// src/stores/firestoreFormat.ts
|
|
1893
|
+
function toFirestoreFormat(schema) {
|
|
1894
|
+
const data = { ...schema };
|
|
1895
|
+
if (schema.orbitals) {
|
|
1896
|
+
data._orbitalsJson = JSON.stringify(schema.orbitals);
|
|
1897
|
+
data.orbitalCount = schema.orbitals.length;
|
|
1898
|
+
delete data.orbitals;
|
|
1899
|
+
}
|
|
1900
|
+
if (data.traits) {
|
|
1901
|
+
const traits = data.traits;
|
|
1902
|
+
data._traitsJson = JSON.stringify(traits);
|
|
1903
|
+
data.traitCount = traits.length;
|
|
1904
|
+
delete data.traits;
|
|
1905
|
+
}
|
|
1906
|
+
if (schema.services) {
|
|
1907
|
+
data._servicesJson = JSON.stringify(schema.services);
|
|
1908
|
+
data.serviceCount = schema.services.length;
|
|
1909
|
+
delete data.services;
|
|
1910
|
+
}
|
|
1911
|
+
return data;
|
|
1912
|
+
}
|
|
1913
|
+
function fromFirestoreFormat(data) {
|
|
1914
|
+
const result = { ...data };
|
|
1915
|
+
if (result._orbitalsJson && typeof result._orbitalsJson === "string") {
|
|
1916
|
+
try {
|
|
1917
|
+
result.orbitals = JSON.parse(result._orbitalsJson);
|
|
1918
|
+
delete result._orbitalsJson;
|
|
1919
|
+
delete result.orbitalCount;
|
|
1920
|
+
} catch (e) {
|
|
1921
|
+
console.warn("[OrbitalStore] Failed to parse _orbitalsJson:", e);
|
|
1922
|
+
result.orbitals = [];
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (result._traitsJson && typeof result._traitsJson === "string") {
|
|
1926
|
+
try {
|
|
1927
|
+
result.traits = JSON.parse(result._traitsJson);
|
|
1928
|
+
delete result._traitsJson;
|
|
1929
|
+
delete result.traitCount;
|
|
1930
|
+
} catch (e) {
|
|
1931
|
+
console.warn("[OrbitalStore] Failed to parse _traitsJson:", e);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (result._servicesJson && typeof result._servicesJson === "string") {
|
|
1935
|
+
try {
|
|
1936
|
+
result.services = JSON.parse(result._servicesJson);
|
|
1937
|
+
delete result._servicesJson;
|
|
1938
|
+
delete result.serviceCount;
|
|
1939
|
+
} catch (e) {
|
|
1940
|
+
console.warn("[OrbitalStore] Failed to parse _servicesJson:", e);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return result;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/stores/SchemaStore.ts
|
|
1947
|
+
init_db();
|
|
1948
|
+
var SchemaProtectionService = class {
|
|
1949
|
+
/**
|
|
1950
|
+
* Compare two schemas and detect destructive changes.
|
|
1951
|
+
*
|
|
1952
|
+
* Returns categorized removals including page content reductions.
|
|
1953
|
+
*/
|
|
1954
|
+
compareSchemas(before, after) {
|
|
1955
|
+
const changeSet = diffSchemas(before, after);
|
|
1956
|
+
const removals = categorizeRemovals(changeSet);
|
|
1957
|
+
const beforePages = before.orbitals?.flatMap((o) => o.pages || []) || [];
|
|
1958
|
+
const afterPages = after.orbitals?.flatMap((o) => o.pages || []) || [];
|
|
1959
|
+
const pageContentReductions = detectPageContentReduction(beforePages, afterPages);
|
|
1960
|
+
removals.pageContentReductions = pageContentReductions;
|
|
1961
|
+
const isDestructive = isDestructiveChange(changeSet) || hasSignificantPageReduction(pageContentReductions);
|
|
1962
|
+
return { isDestructive, removals };
|
|
1963
|
+
}
|
|
1964
|
+
/** Check if critical removals require confirmation */
|
|
1965
|
+
requiresConfirmation(removals) {
|
|
1966
|
+
return requiresConfirmation(removals);
|
|
1967
|
+
}
|
|
1968
|
+
/** Check for significant page content reductions */
|
|
1969
|
+
hasSignificantContentReduction(reductions) {
|
|
1970
|
+
return hasSignificantPageReduction(reductions);
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
|
|
1974
|
+
// src/stores/SchemaStore.ts
|
|
1975
|
+
var SCHEMA_CACHE_TTL_MS = 6e4;
|
|
1976
|
+
var LIST_CACHE_TTL_MS = 3e4;
|
|
1977
|
+
var SchemaStore = class {
|
|
1978
|
+
appsCollection;
|
|
1979
|
+
schemaCache = /* @__PURE__ */ new Map();
|
|
1980
|
+
listCache = /* @__PURE__ */ new Map();
|
|
1981
|
+
protectionService = new SchemaProtectionService();
|
|
1982
|
+
snapshotStore = null;
|
|
1983
|
+
constructor(appsCollection = "apps") {
|
|
1984
|
+
this.appsCollection = appsCollection;
|
|
1985
|
+
}
|
|
1986
|
+
/** Set snapshot store for auto-snapshot on destructive saves */
|
|
1987
|
+
setSnapshotStore(store) {
|
|
1988
|
+
this.snapshotStore = store;
|
|
1989
|
+
}
|
|
1990
|
+
/** Get a schema by app ID */
|
|
1991
|
+
async get(uid, appId) {
|
|
1992
|
+
const cacheKey = `${uid}:${appId}`;
|
|
1993
|
+
const cached = this.schemaCache.get(cacheKey);
|
|
1994
|
+
if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL_MS) {
|
|
1995
|
+
return cached.schema;
|
|
1996
|
+
}
|
|
1997
|
+
try {
|
|
1998
|
+
const db2 = getFirestore();
|
|
1999
|
+
const appDoc = await db2.doc(`users/${uid}/${this.appsCollection}/${appId}`).get();
|
|
2000
|
+
if (!appDoc.exists) return null;
|
|
2001
|
+
const data = appDoc.data();
|
|
2002
|
+
const hasOrbitals = data.orbitals || data._orbitalsJson;
|
|
2003
|
+
if (!data.name || !hasOrbitals) return null;
|
|
2004
|
+
const schema = fromFirestoreFormat(data);
|
|
2005
|
+
this.schemaCache.set(cacheKey, { schema, timestamp: Date.now() });
|
|
2006
|
+
return schema;
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.error("[SchemaStore] Error fetching schema:", error);
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Save a schema (create or full replace).
|
|
2014
|
+
*
|
|
2015
|
+
* Features:
|
|
2016
|
+
* - Detects destructive changes (removals)
|
|
2017
|
+
* - Requires confirmation for critical removals
|
|
2018
|
+
* - Auto-creates snapshots before destructive changes (if SnapshotStore attached)
|
|
2019
|
+
*/
|
|
2020
|
+
async save(uid, appId, schema, options = {}) {
|
|
2021
|
+
try {
|
|
2022
|
+
const existingSchema = await this.get(uid, appId);
|
|
2023
|
+
let snapshotId;
|
|
2024
|
+
if (existingSchema && options.snapshotReason && this.snapshotStore) {
|
|
2025
|
+
snapshotId = await this.snapshotStore.create(uid, appId, existingSchema, options.snapshotReason);
|
|
2026
|
+
}
|
|
2027
|
+
if (existingSchema && !options.skipProtection) {
|
|
2028
|
+
const comparison = this.protectionService.compareSchemas(existingSchema, schema);
|
|
2029
|
+
if (comparison.isDestructive) {
|
|
2030
|
+
const { removals } = comparison;
|
|
2031
|
+
const hasCriticalRemovals = this.protectionService.requiresConfirmation(removals);
|
|
2032
|
+
const hasContentReductions = this.protectionService.hasSignificantContentReduction(
|
|
2033
|
+
removals.pageContentReductions
|
|
2034
|
+
);
|
|
2035
|
+
if ((hasCriticalRemovals || hasContentReductions) && !options.confirmRemovals) {
|
|
2036
|
+
return {
|
|
2037
|
+
success: false,
|
|
2038
|
+
requiresConfirmation: true,
|
|
2039
|
+
removals: comparison.removals,
|
|
2040
|
+
error: hasContentReductions ? "Page content reduction detected - confirmation required" : "Confirmation required for critical removals"
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
if (!snapshotId && this.snapshotStore && (removals.critical.length > 0 || removals.pageContentReductions.length > 0)) {
|
|
2044
|
+
snapshotId = await this.snapshotStore.create(
|
|
2045
|
+
uid,
|
|
2046
|
+
appId,
|
|
2047
|
+
existingSchema,
|
|
2048
|
+
`auto_before_removal_${Date.now()}`
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const firestoreData = toFirestoreFormat(schema);
|
|
2054
|
+
const now = Date.now();
|
|
2055
|
+
const docData = {
|
|
2056
|
+
...firestoreData,
|
|
2057
|
+
_metadata: {
|
|
2058
|
+
version: options.expectedVersion ? options.expectedVersion + 1 : 1,
|
|
2059
|
+
updatedAt: now,
|
|
2060
|
+
createdAt: existingSchema ? void 0 : now,
|
|
2061
|
+
source: options.source || "manual"
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
const db2 = getFirestore();
|
|
2065
|
+
await db2.doc(`users/${uid}/${this.appsCollection}/${appId}`).set(docData, { merge: true });
|
|
2066
|
+
this.invalidateCache(uid, appId);
|
|
2067
|
+
return { success: true, snapshotId };
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
console.error("[SchemaStore] Error saving schema:", error);
|
|
2070
|
+
return {
|
|
2071
|
+
success: false,
|
|
2072
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/** Create a new app with initial schema */
|
|
2077
|
+
async create(uid, metadata) {
|
|
2078
|
+
const appId = `app-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2079
|
+
const now = Date.now();
|
|
2080
|
+
const schema = {
|
|
2081
|
+
name: metadata.name,
|
|
2082
|
+
description: metadata.description,
|
|
2083
|
+
orbitals: []
|
|
2084
|
+
};
|
|
2085
|
+
const firestoreData = toFirestoreFormat(schema);
|
|
2086
|
+
const docData = {
|
|
2087
|
+
...firestoreData,
|
|
2088
|
+
_metadata: { version: 1, createdAt: now, updatedAt: now, source: "manual" }
|
|
2089
|
+
};
|
|
2090
|
+
const db2 = getFirestore();
|
|
2091
|
+
await db2.doc(`users/${uid}/${this.appsCollection}/${appId}`).set(docData);
|
|
2092
|
+
this.listCache.delete(uid);
|
|
2093
|
+
return { appId, schema };
|
|
2094
|
+
}
|
|
2095
|
+
/** Delete an app */
|
|
2096
|
+
async delete(uid, appId) {
|
|
2097
|
+
try {
|
|
2098
|
+
const db2 = getFirestore();
|
|
2099
|
+
const ref = db2.doc(`users/${uid}/${this.appsCollection}/${appId}`);
|
|
2100
|
+
const doc = await ref.get();
|
|
2101
|
+
if (!doc.exists) return false;
|
|
2102
|
+
await ref.delete();
|
|
2103
|
+
this.invalidateCache(uid, appId);
|
|
2104
|
+
return true;
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
console.error("[SchemaStore] Error deleting app:", error);
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
/** List all apps for a user */
|
|
2111
|
+
async list(uid) {
|
|
2112
|
+
const cached = this.listCache.get(uid);
|
|
2113
|
+
if (cached && Date.now() - cached.timestamp < LIST_CACHE_TTL_MS) {
|
|
2114
|
+
return cached.apps;
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
const db2 = getFirestore();
|
|
2118
|
+
const snapshot = await db2.collection(`users/${uid}/${this.appsCollection}`).select("name", "description", "domainContext", "_metadata", "orbitalCount", "traitCount").orderBy("_metadata.updatedAt", "desc").get();
|
|
2119
|
+
const apps = snapshot.docs.map((doc) => {
|
|
2120
|
+
const data = doc.data();
|
|
2121
|
+
const metadata = data._metadata;
|
|
2122
|
+
const orbitalCount = data.orbitalCount;
|
|
2123
|
+
return {
|
|
2124
|
+
id: doc.id,
|
|
2125
|
+
name: data.name || "Untitled",
|
|
2126
|
+
description: data.description,
|
|
2127
|
+
updatedAt: metadata?.updatedAt || Date.now(),
|
|
2128
|
+
createdAt: metadata?.createdAt || Date.now(),
|
|
2129
|
+
stats: { entities: orbitalCount ?? 0, pages: 0, states: 0, events: 0, transitions: 0 },
|
|
2130
|
+
domainContext: data.domainContext,
|
|
2131
|
+
hasValidationErrors: false
|
|
2132
|
+
};
|
|
2133
|
+
});
|
|
2134
|
+
this.listCache.set(uid, { apps, timestamp: Date.now() });
|
|
2135
|
+
return apps;
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
console.error("[SchemaStore] Error listing apps:", error);
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
/** Compute stats from OrbitalSchema */
|
|
2142
|
+
computeStats(schema) {
|
|
2143
|
+
const orbitals = schema.orbitals || [];
|
|
2144
|
+
const entities = orbitals.length;
|
|
2145
|
+
const pages = orbitals.reduce((n, o) => n + (o.pages?.length || 0), 0);
|
|
2146
|
+
const allTraits = [
|
|
2147
|
+
...schema.traits || [],
|
|
2148
|
+
...orbitals.flatMap(
|
|
2149
|
+
(o) => (o.traits || []).filter((t) => typeof t !== "string" && "stateMachine" in t)
|
|
2150
|
+
)
|
|
2151
|
+
];
|
|
2152
|
+
return {
|
|
2153
|
+
states: allTraits.flatMap((t) => t.stateMachine?.states || []).length,
|
|
2154
|
+
events: allTraits.flatMap((t) => t.stateMachine?.events || []).length,
|
|
2155
|
+
pages,
|
|
2156
|
+
entities,
|
|
2157
|
+
transitions: allTraits.flatMap((t) => t.stateMachine?.transitions || []).length
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
/** Invalidate caches for a specific app */
|
|
2161
|
+
invalidateCache(uid, appId) {
|
|
2162
|
+
this.schemaCache.delete(`${uid}:${appId}`);
|
|
2163
|
+
this.listCache.delete(uid);
|
|
2164
|
+
}
|
|
2165
|
+
/** Clear all caches */
|
|
2166
|
+
clearCaches() {
|
|
2167
|
+
this.schemaCache.clear();
|
|
2168
|
+
this.listCache.clear();
|
|
2169
|
+
}
|
|
2170
|
+
/** Get the collection path for an app */
|
|
2171
|
+
getAppDocPath(uid, appId) {
|
|
2172
|
+
return `users/${uid}/${this.appsCollection}/${appId}`;
|
|
2173
|
+
}
|
|
2174
|
+
/** Expose apps collection name for subcollection stores */
|
|
2175
|
+
getAppsCollection() {
|
|
2176
|
+
return this.appsCollection;
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
// src/stores/SnapshotStore.ts
|
|
2181
|
+
init_db();
|
|
2182
|
+
var SNAPSHOTS_COLLECTION = "snapshots";
|
|
2183
|
+
var SnapshotStore = class {
|
|
2184
|
+
appsCollection;
|
|
2185
|
+
constructor(appsCollection = "apps") {
|
|
2186
|
+
this.appsCollection = appsCollection;
|
|
2187
|
+
}
|
|
2188
|
+
getCollectionPath(uid, appId) {
|
|
2189
|
+
return `users/${uid}/${this.appsCollection}/${appId}/${SNAPSHOTS_COLLECTION}`;
|
|
2190
|
+
}
|
|
2191
|
+
getAppDocPath(uid, appId) {
|
|
2192
|
+
return `users/${uid}/${this.appsCollection}/${appId}`;
|
|
2193
|
+
}
|
|
2194
|
+
/** Create a snapshot of the current schema */
|
|
2195
|
+
async create(uid, appId, schema, reason) {
|
|
2196
|
+
const db2 = getFirestore();
|
|
2197
|
+
const snapshotId = `snapshot_${Date.now()}`;
|
|
2198
|
+
const snapshotDoc = {
|
|
2199
|
+
id: snapshotId,
|
|
2200
|
+
timestamp: Date.now(),
|
|
2201
|
+
schema: toFirestoreFormat(schema),
|
|
2202
|
+
reason
|
|
2203
|
+
};
|
|
2204
|
+
await db2.doc(`${this.getCollectionPath(uid, appId)}/${snapshotId}`).set(snapshotDoc);
|
|
2205
|
+
const appDocRef = db2.doc(this.getAppDocPath(uid, appId));
|
|
2206
|
+
const appDoc = await appDocRef.get();
|
|
2207
|
+
const currentMeta = appDoc.data()?._historyMeta;
|
|
2208
|
+
const updatedMeta = {
|
|
2209
|
+
latestSnapshotId: snapshotId,
|
|
2210
|
+
latestChangeSetId: currentMeta?.latestChangeSetId,
|
|
2211
|
+
snapshotCount: (currentMeta?.snapshotCount || 0) + 1,
|
|
2212
|
+
changeSetCount: currentMeta?.changeSetCount || 0
|
|
2213
|
+
};
|
|
2214
|
+
await appDocRef.set({ _historyMeta: updatedMeta }, { merge: true });
|
|
2215
|
+
return snapshotId;
|
|
2216
|
+
}
|
|
2217
|
+
/** Get all snapshots for an app (ordered by timestamp desc) */
|
|
2218
|
+
async getAll(uid, appId) {
|
|
2219
|
+
const db2 = getFirestore();
|
|
2220
|
+
const query = await db2.collection(this.getCollectionPath(uid, appId)).orderBy("timestamp", "desc").get();
|
|
2221
|
+
return query.docs.map((doc) => doc.data());
|
|
2222
|
+
}
|
|
2223
|
+
/** Get a specific snapshot by ID */
|
|
2224
|
+
async get(uid, appId, snapshotId) {
|
|
2225
|
+
const db2 = getFirestore();
|
|
2226
|
+
const doc = await db2.doc(`${this.getCollectionPath(uid, appId)}/${snapshotId}`).get();
|
|
2227
|
+
if (!doc.exists) return null;
|
|
2228
|
+
return doc.data();
|
|
2229
|
+
}
|
|
2230
|
+
/** Delete a snapshot */
|
|
2231
|
+
async delete(uid, appId, snapshotId) {
|
|
2232
|
+
const db2 = getFirestore();
|
|
2233
|
+
const ref = db2.doc(`${this.getCollectionPath(uid, appId)}/${snapshotId}`);
|
|
2234
|
+
const doc = await ref.get();
|
|
2235
|
+
if (!doc.exists) return false;
|
|
2236
|
+
await ref.delete();
|
|
2237
|
+
const appDocRef = db2.doc(this.getAppDocPath(uid, appId));
|
|
2238
|
+
const appDoc = await appDocRef.get();
|
|
2239
|
+
const currentMeta = appDoc.data()?._historyMeta;
|
|
2240
|
+
if (currentMeta) {
|
|
2241
|
+
const updatedMeta = {
|
|
2242
|
+
...currentMeta,
|
|
2243
|
+
snapshotCount: Math.max(0, (currentMeta.snapshotCount || 1) - 1),
|
|
2244
|
+
latestSnapshotId: currentMeta.latestSnapshotId === snapshotId ? void 0 : currentMeta.latestSnapshotId
|
|
2245
|
+
};
|
|
2246
|
+
await appDocRef.set({ _historyMeta: updatedMeta }, { merge: true });
|
|
2247
|
+
}
|
|
2248
|
+
return true;
|
|
2249
|
+
}
|
|
2250
|
+
/** Get schema snapshot at a specific version */
|
|
2251
|
+
async getByVersion(uid, appId, version) {
|
|
2252
|
+
const db2 = getFirestore();
|
|
2253
|
+
const query = await db2.collection(this.getCollectionPath(uid, appId)).where("version", "==", version).limit(1).get();
|
|
2254
|
+
if (query.empty) return null;
|
|
2255
|
+
const snapshot = query.docs[0].data();
|
|
2256
|
+
return fromFirestoreFormat(snapshot.schema);
|
|
2257
|
+
}
|
|
2258
|
+
/** Get the schema from a snapshot (deserialized) */
|
|
2259
|
+
getSchemaFromSnapshot(snapshot) {
|
|
2260
|
+
return fromFirestoreFormat(snapshot.schema);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// src/stores/ChangeSetStore.ts
|
|
2265
|
+
init_db();
|
|
2266
|
+
var CHANGESETS_COLLECTION = "changesets";
|
|
2267
|
+
var ChangeSetStore = class {
|
|
2268
|
+
appsCollection;
|
|
2269
|
+
constructor(appsCollection = "apps") {
|
|
2270
|
+
this.appsCollection = appsCollection;
|
|
2271
|
+
}
|
|
2272
|
+
getCollectionPath(uid, appId) {
|
|
2273
|
+
return `users/${uid}/${this.appsCollection}/${appId}/${CHANGESETS_COLLECTION}`;
|
|
2274
|
+
}
|
|
2275
|
+
getAppDocPath(uid, appId) {
|
|
2276
|
+
return `users/${uid}/${this.appsCollection}/${appId}`;
|
|
2277
|
+
}
|
|
2278
|
+
/** Append a changeset to history */
|
|
2279
|
+
async append(uid, appId, changeSet) {
|
|
2280
|
+
const db2 = getFirestore();
|
|
2281
|
+
await db2.doc(`${this.getCollectionPath(uid, appId)}/${changeSet.id}`).set(changeSet);
|
|
2282
|
+
const appDocRef = db2.doc(this.getAppDocPath(uid, appId));
|
|
2283
|
+
const appDoc = await appDocRef.get();
|
|
2284
|
+
const currentMeta = appDoc.data()?._historyMeta;
|
|
2285
|
+
const updatedMeta = {
|
|
2286
|
+
latestSnapshotId: currentMeta?.latestSnapshotId,
|
|
2287
|
+
latestChangeSetId: changeSet.id,
|
|
2288
|
+
snapshotCount: currentMeta?.snapshotCount || 0,
|
|
2289
|
+
changeSetCount: (currentMeta?.changeSetCount || 0) + 1
|
|
2290
|
+
};
|
|
2291
|
+
await appDocRef.set({ _historyMeta: updatedMeta }, { merge: true });
|
|
2292
|
+
}
|
|
2293
|
+
/** Get change history for an app (ordered by version desc) */
|
|
2294
|
+
async getHistory(uid, appId) {
|
|
2295
|
+
try {
|
|
2296
|
+
const db2 = getFirestore();
|
|
2297
|
+
const query = await db2.collection(this.getCollectionPath(uid, appId)).orderBy("version", "desc").get();
|
|
2298
|
+
return query.docs.map((doc) => doc.data());
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
console.error("[ChangeSetStore] Error getting change history:", error);
|
|
2301
|
+
return [];
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
/** Get a specific changeset by ID */
|
|
2305
|
+
async get(uid, appId, changeSetId) {
|
|
2306
|
+
const db2 = getFirestore();
|
|
2307
|
+
const doc = await db2.doc(`${this.getCollectionPath(uid, appId)}/${changeSetId}`).get();
|
|
2308
|
+
if (!doc.exists) return null;
|
|
2309
|
+
return doc.data();
|
|
2310
|
+
}
|
|
2311
|
+
/** Update a changeset's status */
|
|
2312
|
+
async updateStatus(uid, appId, changeSetId, status) {
|
|
2313
|
+
const db2 = getFirestore();
|
|
2314
|
+
const ref = db2.doc(`${this.getCollectionPath(uid, appId)}/${changeSetId}`);
|
|
2315
|
+
const doc = await ref.get();
|
|
2316
|
+
if (!doc.exists) return;
|
|
2317
|
+
await ref.update({ status });
|
|
2318
|
+
}
|
|
2319
|
+
/** Delete a changeset */
|
|
2320
|
+
async delete(uid, appId, changeSetId) {
|
|
2321
|
+
const db2 = getFirestore();
|
|
2322
|
+
const ref = db2.doc(`${this.getCollectionPath(uid, appId)}/${changeSetId}`);
|
|
2323
|
+
const doc = await ref.get();
|
|
2324
|
+
if (!doc.exists) return false;
|
|
2325
|
+
await ref.delete();
|
|
2326
|
+
const appDocRef = db2.doc(this.getAppDocPath(uid, appId));
|
|
2327
|
+
const appDoc = await appDocRef.get();
|
|
2328
|
+
const currentMeta = appDoc.data()?._historyMeta;
|
|
2329
|
+
if (currentMeta) {
|
|
2330
|
+
const updatedMeta = {
|
|
2331
|
+
...currentMeta,
|
|
2332
|
+
changeSetCount: Math.max(0, (currentMeta.changeSetCount || 1) - 1),
|
|
2333
|
+
latestChangeSetId: currentMeta.latestChangeSetId === changeSetId ? void 0 : currentMeta.latestChangeSetId
|
|
2334
|
+
};
|
|
2335
|
+
await appDocRef.set({ _historyMeta: updatedMeta }, { merge: true });
|
|
2336
|
+
}
|
|
2337
|
+
return true;
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
// src/stores/ValidationStore.ts
|
|
2342
|
+
init_db();
|
|
2343
|
+
var VALIDATION_COLLECTION = "validation";
|
|
2344
|
+
var VALIDATION_DOC_ID = "current";
|
|
2345
|
+
var ValidationStore = class {
|
|
2346
|
+
appsCollection;
|
|
2347
|
+
constructor(appsCollection = "apps") {
|
|
2348
|
+
this.appsCollection = appsCollection;
|
|
2349
|
+
}
|
|
2350
|
+
getDocPath(uid, appId) {
|
|
2351
|
+
return `users/${uid}/${this.appsCollection}/${appId}/${VALIDATION_COLLECTION}/${VALIDATION_DOC_ID}`;
|
|
2352
|
+
}
|
|
2353
|
+
getAppDocPath(uid, appId) {
|
|
2354
|
+
return `users/${uid}/${this.appsCollection}/${appId}`;
|
|
2355
|
+
}
|
|
2356
|
+
/** Save validation results */
|
|
2357
|
+
async save(uid, appId, results) {
|
|
2358
|
+
const db2 = getFirestore();
|
|
2359
|
+
await db2.doc(this.getDocPath(uid, appId)).set(results);
|
|
2360
|
+
const validationMeta = {
|
|
2361
|
+
errorCount: results.errors?.length || 0,
|
|
2362
|
+
warningCount: results.warnings?.length || 0,
|
|
2363
|
+
validatedAt: results.validatedAt
|
|
2364
|
+
};
|
|
2365
|
+
await db2.doc(this.getAppDocPath(uid, appId)).set(
|
|
2366
|
+
{ _operational: { validationMeta } },
|
|
2367
|
+
{ merge: true }
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
/** Get validation results */
|
|
2371
|
+
async get(uid, appId) {
|
|
2372
|
+
try {
|
|
2373
|
+
const db2 = getFirestore();
|
|
2374
|
+
const doc = await db2.doc(this.getDocPath(uid, appId)).get();
|
|
2375
|
+
if (!doc.exists) return null;
|
|
2376
|
+
return doc.data();
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
console.error("[ValidationStore] Error getting validation results:", error);
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
/** Clear validation results */
|
|
2383
|
+
async clear(uid, appId) {
|
|
2384
|
+
const db2 = getFirestore();
|
|
2385
|
+
const { FieldValue } = await import('firebase-admin/firestore');
|
|
2386
|
+
await db2.doc(this.getDocPath(uid, appId)).delete();
|
|
2387
|
+
await db2.doc(this.getAppDocPath(uid, appId)).update({
|
|
2388
|
+
"_operational.validationMeta": FieldValue.delete()
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
// src/lib/serviceDiscovery.ts
|
|
2394
|
+
var InMemoryServiceRegistry = class {
|
|
2395
|
+
services = /* @__PURE__ */ new Map();
|
|
2396
|
+
async register(service) {
|
|
2397
|
+
this.services.set(service.instanceId, {
|
|
2398
|
+
...service,
|
|
2399
|
+
registeredAt: Date.now(),
|
|
2400
|
+
lastHeartbeat: Date.now()
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
async deregister(instanceId) {
|
|
2404
|
+
this.services.delete(instanceId);
|
|
2405
|
+
}
|
|
2406
|
+
async heartbeat(instanceId) {
|
|
2407
|
+
const service = this.services.get(instanceId);
|
|
2408
|
+
if (service) {
|
|
2409
|
+
service.lastHeartbeat = Date.now();
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
async updateStatus(instanceId, status) {
|
|
2413
|
+
const service = this.services.get(instanceId);
|
|
2414
|
+
if (service) {
|
|
2415
|
+
service.status = status;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
async getAll() {
|
|
2419
|
+
return Array.from(this.services.values());
|
|
2420
|
+
}
|
|
2421
|
+
async getByName(name) {
|
|
2422
|
+
return Array.from(this.services.values()).filter((s) => s.name === name);
|
|
2423
|
+
}
|
|
2424
|
+
async findListeners(event) {
|
|
2425
|
+
return Array.from(this.services.values()).filter(
|
|
2426
|
+
(s) => s.listens.includes(event) && s.status !== "stopping"
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
async findEmitters(event) {
|
|
2430
|
+
return Array.from(this.services.values()).filter(
|
|
2431
|
+
(s) => s.emits.includes(event) && s.status !== "stopping"
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
async cleanup(ttlMs) {
|
|
2435
|
+
const cutoff = Date.now() - ttlMs;
|
|
2436
|
+
let removed = 0;
|
|
2437
|
+
for (const [id, service] of this.services) {
|
|
2438
|
+
if (service.lastHeartbeat < cutoff) {
|
|
2439
|
+
this.services.delete(id);
|
|
2440
|
+
removed++;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return removed;
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
var ServiceDiscovery = class {
|
|
2447
|
+
registry;
|
|
2448
|
+
options;
|
|
2449
|
+
cleanupTimer = null;
|
|
2450
|
+
constructor(options, registry) {
|
|
2451
|
+
this.options = {
|
|
2452
|
+
heartbeatTtlMs: options?.heartbeatTtlMs ?? 6e4,
|
|
2453
|
+
cleanupIntervalMs: options?.cleanupIntervalMs ?? 3e4
|
|
2454
|
+
};
|
|
2455
|
+
this.registry = registry ?? new InMemoryServiceRegistry();
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Register a service with the registry.
|
|
2459
|
+
*/
|
|
2460
|
+
async register(service) {
|
|
2461
|
+
await this.registry.register({
|
|
2462
|
+
...service,
|
|
2463
|
+
status: service.status ?? "starting",
|
|
2464
|
+
registeredAt: Date.now(),
|
|
2465
|
+
lastHeartbeat: Date.now()
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Deregister a service.
|
|
2470
|
+
*/
|
|
2471
|
+
async deregister(instanceId) {
|
|
2472
|
+
await this.registry.deregister(instanceId);
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Send heartbeat for a service.
|
|
2476
|
+
*/
|
|
2477
|
+
async heartbeat(instanceId) {
|
|
2478
|
+
await this.registry.heartbeat(instanceId);
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Mark a service as ready.
|
|
2482
|
+
*/
|
|
2483
|
+
async markReady(instanceId) {
|
|
2484
|
+
await this.registry.updateStatus(instanceId, "ready");
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Mark a service as degraded.
|
|
2488
|
+
*/
|
|
2489
|
+
async markDegraded(instanceId) {
|
|
2490
|
+
await this.registry.updateStatus(instanceId, "degraded");
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Find all services that listen for a given event.
|
|
2494
|
+
*/
|
|
2495
|
+
async findListeners(event) {
|
|
2496
|
+
return this.registry.findListeners(event);
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Find all services that emit a given event.
|
|
2500
|
+
*/
|
|
2501
|
+
async findEmitters(event) {
|
|
2502
|
+
return this.registry.findEmitters(event);
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Get all registered services.
|
|
2506
|
+
*/
|
|
2507
|
+
async getAll() {
|
|
2508
|
+
return this.registry.getAll();
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Get the full event topology (who emits what, who listens for what).
|
|
2512
|
+
*/
|
|
2513
|
+
async getEventTopology() {
|
|
2514
|
+
const services = await this.registry.getAll();
|
|
2515
|
+
const eventMap = /* @__PURE__ */ new Map();
|
|
2516
|
+
for (const service of services) {
|
|
2517
|
+
for (const event of service.emits) {
|
|
2518
|
+
if (!eventMap.has(event)) eventMap.set(event, { emitters: /* @__PURE__ */ new Set(), listeners: /* @__PURE__ */ new Set() });
|
|
2519
|
+
eventMap.get(event).emitters.add(service.name);
|
|
2520
|
+
}
|
|
2521
|
+
for (const event of service.listens) {
|
|
2522
|
+
if (!eventMap.has(event)) eventMap.set(event, { emitters: /* @__PURE__ */ new Set(), listeners: /* @__PURE__ */ new Set() });
|
|
2523
|
+
eventMap.get(event).listeners.add(service.name);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
const events = Array.from(eventMap.entries()).map(([event, { emitters, listeners }]) => ({
|
|
2527
|
+
event,
|
|
2528
|
+
emitters: Array.from(emitters),
|
|
2529
|
+
listeners: Array.from(listeners)
|
|
2530
|
+
}));
|
|
2531
|
+
return { events };
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Start periodic cleanup of expired services.
|
|
2535
|
+
*/
|
|
2536
|
+
startCleanup() {
|
|
2537
|
+
if (this.cleanupTimer) return;
|
|
2538
|
+
this.cleanupTimer = setInterval(() => {
|
|
2539
|
+
this.registry.cleanup(this.options.heartbeatTtlMs).catch((err) => {
|
|
2540
|
+
console.error("[ServiceDiscovery] Cleanup error:", err);
|
|
2541
|
+
});
|
|
2542
|
+
}, this.options.cleanupIntervalMs);
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Stop periodic cleanup.
|
|
2546
|
+
*/
|
|
2547
|
+
stopCleanup() {
|
|
2548
|
+
if (this.cleanupTimer) {
|
|
2549
|
+
clearInterval(this.cleanupTimer);
|
|
2550
|
+
this.cleanupTimer = null;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Get the underlying registry (for testing).
|
|
2555
|
+
*/
|
|
2556
|
+
getRegistry() {
|
|
2557
|
+
return this.registry;
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
// src/index.ts
|
|
2562
|
+
var dataService = new Proxy({}, {
|
|
2563
|
+
get(_target, prop, receiver) {
|
|
2564
|
+
return Reflect.get(getDataService(), prop, receiver);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
var mockDataService = new Proxy({}, {
|
|
2568
|
+
get(_target, prop, receiver) {
|
|
2569
|
+
return Reflect.get(getMockDataService(), prop, receiver);
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
var serverEventBus = new Proxy({}, {
|
|
2573
|
+
get(_target, prop, receiver) {
|
|
2574
|
+
return Reflect.get(getServerEventBus(), prop, receiver);
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
2577
|
+
async function getMemoryManager2(...args) {
|
|
2578
|
+
const m = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
2579
|
+
return m.getMemoryManager(...args);
|
|
2580
|
+
}
|
|
2581
|
+
async function resetMemoryManager2() {
|
|
2582
|
+
const m = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
2583
|
+
return m.resetMemoryManager();
|
|
2584
|
+
}
|
|
2585
|
+
async function getSessionManager2(...args) {
|
|
2586
|
+
const m = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
2587
|
+
return m.getSessionManager(...args);
|
|
2588
|
+
}
|
|
2589
|
+
async function resetSessionManager2() {
|
|
2590
|
+
const m = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
2591
|
+
return m.resetSessionManager();
|
|
2592
|
+
}
|
|
2593
|
+
async function createServerSkillAgent2(options) {
|
|
2594
|
+
const m = await Promise.resolve().then(() => (init_skill_agent(), skill_agent_exports));
|
|
2595
|
+
return m.createServerSkillAgent(options);
|
|
2596
|
+
}
|
|
2597
|
+
async function multiUserMiddleware2(req, res, next) {
|
|
2598
|
+
const m = await Promise.resolve().then(() => (init_multi_user(), multi_user_exports));
|
|
2599
|
+
return m.multiUserMiddleware(req, res, next);
|
|
2600
|
+
}
|
|
2601
|
+
async function verifyFirebaseAuth2(req, res, next) {
|
|
2602
|
+
const m = await Promise.resolve().then(() => (init_multi_user(), multi_user_exports));
|
|
2603
|
+
return m.verifyFirebaseAuth(req, res, next);
|
|
2604
|
+
}
|
|
2605
|
+
async function setupStateSyncWebSocket2(io) {
|
|
2606
|
+
const m = await Promise.resolve().then(() => (init_state_sync(), state_sync_exports));
|
|
2607
|
+
return m.setupStateSyncWebSocket(io);
|
|
2608
|
+
}
|
|
2609
|
+
async function observabilityRouter() {
|
|
2610
|
+
const m = await Promise.resolve().then(() => (init_observability(), observability_exports));
|
|
2611
|
+
return m.default;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
export { AppError, ChangeSetStore, ConflictError, DistributedEventBus, EventBus, EventPersistence, ForbiddenError, InMemoryEventStore, InMemoryServiceRegistry, InMemoryTransport, MockDataService, NotFoundError, RedisTransport, SchemaProtectionService, SchemaStore, ServiceDiscovery, SnapshotStore, UnauthorizedError, ValidationError, ValidationStore, applyFiltersToQuery, asyncHandler, authenticateFirebase, closeWebSocketServer, createServerSkillAgent2 as createServerSkillAgent, dataService, db, debugEventsRouter, emitEntityEvent, env, errorHandler, extractPaginationParams, fromFirestoreFormat, getAuth, getConnectedClientCount, getDataService, getFirestore, getMemoryManager2 as getMemoryManager, getMockDataService, getServerEventBus, getSessionManager2 as getSessionManager, getWebSocketServer, initializeFirebase, logger, mockDataService, multiUserMiddleware2 as multiUserMiddleware, notFoundHandler, observabilityRouter, parseQueryFilters, resetDataService, resetMemoryManager2 as resetMemoryManager, resetMockDataService, resetServerEventBus, resetSessionManager2 as resetSessionManager, seedMockData, serverEventBus, setupEventBroadcast, setupStateSyncWebSocket2 as setupStateSyncWebSocket, toFirestoreFormat, validateBody, validateParams, validateQuery, verifyFirebaseAuth2 as verifyFirebaseAuth };
|
|
2615
|
+
//# sourceMappingURL=index.js.map
|
|
2616
|
+
//# sourceMappingURL=index.js.map
|