@fitzzero/quickdraw-core 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -28
- package/dist/client/index.d.ts +30 -37
- package/dist/client/index.js +103 -58
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/client/testing.js +32 -0
- package/dist/client/testing.js.map +1 -1
- package/dist/server/index.d.ts +577 -9
- package/dist/server/index.js +985 -47
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing.d.ts +2 -2
- package/dist/server/testing.js +124 -36
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.d.ts +111 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/{types-CUExGF7F.d.ts → types-BADirRXn.d.ts} +50 -8
- package/dist/{types-OhRxDBID.d.ts → types-D7oHWh4O.d.ts} +60 -2
- package/package.json +12 -13
package/dist/server/testing.js
CHANGED
|
@@ -19,47 +19,84 @@ var ServiceRegistry = class {
|
|
|
19
19
|
io;
|
|
20
20
|
services = /* @__PURE__ */ new Map();
|
|
21
21
|
logger;
|
|
22
|
+
methodLoggingEnabled;
|
|
23
|
+
logPayloads;
|
|
24
|
+
logResponses;
|
|
25
|
+
connectionHandlerInstalled = false;
|
|
26
|
+
// Store method metadata for deferred registration
|
|
27
|
+
methodRegistry = /* @__PURE__ */ new Map();
|
|
22
28
|
constructor(io, options) {
|
|
23
29
|
this.io = io;
|
|
24
30
|
this.logger = options?.logger?.child({ service: "ServiceRegistry" }) ?? consoleLogger.child({ service: "ServiceRegistry" });
|
|
31
|
+
this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;
|
|
32
|
+
this.logPayloads = options?.methodLogging?.logPayloads ?? false;
|
|
33
|
+
this.logResponses = options?.methodLogging?.logResponses ?? false;
|
|
34
|
+
if (this.methodLoggingEnabled) {
|
|
35
|
+
this.logger.info("Automatic method logging enabled", {
|
|
36
|
+
logPayloads: this.logPayloads,
|
|
37
|
+
logResponses: this.logResponses
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Install the single connection handler that wires all registered services.
|
|
43
|
+
* This is called lazily on first service registration.
|
|
44
|
+
*/
|
|
45
|
+
installConnectionHandler() {
|
|
46
|
+
if (this.connectionHandlerInstalled) return;
|
|
47
|
+
this.connectionHandlerInstalled = true;
|
|
48
|
+
this.io.on("connection", (socket) => {
|
|
49
|
+
const quickdrawSocket = socket;
|
|
50
|
+
this.setupSocketListeners(quickdrawSocket);
|
|
51
|
+
});
|
|
52
|
+
this.logger.info("Installed single connection handler for all services");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Set up all event listeners for a newly connected socket.
|
|
56
|
+
*/
|
|
57
|
+
setupSocketListeners(socket) {
|
|
58
|
+
for (const [eventName, { method, service }] of this.methodRegistry) {
|
|
59
|
+
this.registerMethodListener(socket, eventName, method, service);
|
|
60
|
+
}
|
|
61
|
+
for (const [serviceName, service] of this.services) {
|
|
62
|
+
this.registerSubscriptionListener(
|
|
63
|
+
socket,
|
|
64
|
+
`${serviceName}:subscribe`,
|
|
65
|
+
service
|
|
66
|
+
);
|
|
67
|
+
this.registerUnsubscriptionListener(
|
|
68
|
+
socket,
|
|
69
|
+
`${serviceName}:unsubscribe`,
|
|
70
|
+
service
|
|
71
|
+
);
|
|
72
|
+
}
|
|
25
73
|
}
|
|
26
74
|
/**
|
|
27
75
|
* Register a service and auto-discover its public methods.
|
|
28
76
|
*/
|
|
29
77
|
registerService(serviceName, service) {
|
|
30
78
|
this.services.set(serviceName, service);
|
|
79
|
+
if (service.setIo) {
|
|
80
|
+
service.setIo(this.io);
|
|
81
|
+
}
|
|
31
82
|
this.logger.info(`Registered service: ${serviceName}`);
|
|
32
83
|
this.discoverServiceMethods(serviceName, service);
|
|
84
|
+
this.installConnectionHandler();
|
|
33
85
|
}
|
|
34
86
|
/**
|
|
35
|
-
* Discover and
|
|
87
|
+
* Discover and store all public methods from a service.
|
|
36
88
|
*/
|
|
37
89
|
discoverServiceMethods(serviceName, service) {
|
|
38
90
|
const methods = service.getPublicMethods();
|
|
39
91
|
for (const method of methods) {
|
|
40
92
|
const eventName = `${serviceName}:${method.name}`;
|
|
41
93
|
this.logger.info(`Registering socket event: ${eventName}`);
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
method,
|
|
47
|
-
service
|
|
48
|
-
);
|
|
94
|
+
this.methodRegistry.set(eventName, {
|
|
95
|
+
serviceName,
|
|
96
|
+
method,
|
|
97
|
+
service
|
|
49
98
|
});
|
|
50
99
|
}
|
|
51
|
-
this.io.on("connection", (socket) => {
|
|
52
|
-
this.registerSubscriptionListener(
|
|
53
|
-
socket,
|
|
54
|
-
`${serviceName}:subscribe`,
|
|
55
|
-
service
|
|
56
|
-
);
|
|
57
|
-
this.registerUnsubscriptionListener(
|
|
58
|
-
socket,
|
|
59
|
-
`${serviceName}:unsubscribe`,
|
|
60
|
-
service
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
100
|
}
|
|
64
101
|
/**
|
|
65
102
|
* Register a method as a socket event listener.
|
|
@@ -105,33 +142,45 @@ var ServiceRegistry = class {
|
|
|
105
142
|
}
|
|
106
143
|
}
|
|
107
144
|
await service.ensureAccessForMethod(method.access, socket, entryId);
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
{
|
|
145
|
+
if (this.methodLoggingEnabled) {
|
|
146
|
+
const logContext = {
|
|
111
147
|
category: "request_processing",
|
|
112
148
|
serviceName,
|
|
113
149
|
methodName,
|
|
114
150
|
userId: socket.userId,
|
|
115
151
|
socketId: socket.id
|
|
152
|
+
};
|
|
153
|
+
if (this.logPayloads) {
|
|
154
|
+
logContext.payload = validatedPayload;
|
|
116
155
|
}
|
|
117
|
-
|
|
156
|
+
this.logger.info(
|
|
157
|
+
`User ${userIdShort} ${serviceName}.${methodName} -> start`,
|
|
158
|
+
logContext
|
|
159
|
+
);
|
|
160
|
+
}
|
|
118
161
|
const result = await method.handler(validatedPayload, {
|
|
119
162
|
userId: socket.userId,
|
|
120
163
|
socketId: socket.id,
|
|
121
164
|
serviceAccess: socket.serviceAccess ?? {}
|
|
122
165
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
166
|
+
if (this.methodLoggingEnabled) {
|
|
167
|
+
const durationMs = Date.now() - startedAt;
|
|
168
|
+
const logContext = {
|
|
127
169
|
category: "request_processing",
|
|
128
170
|
outcome: "success",
|
|
129
171
|
durationMs,
|
|
130
172
|
serviceName,
|
|
131
173
|
methodName,
|
|
132
174
|
userId: socket.userId
|
|
175
|
+
};
|
|
176
|
+
if (this.logResponses) {
|
|
177
|
+
logContext.response = result;
|
|
133
178
|
}
|
|
134
|
-
|
|
179
|
+
this.logger.info(
|
|
180
|
+
`User ${userIdShort} ${serviceName}.${methodName} -> success`,
|
|
181
|
+
logContext
|
|
182
|
+
);
|
|
183
|
+
}
|
|
135
184
|
const successResponse = {
|
|
136
185
|
success: true,
|
|
137
186
|
data: result
|
|
@@ -139,14 +188,22 @@ var ServiceRegistry = class {
|
|
|
139
188
|
callback?.(successResponse);
|
|
140
189
|
} catch (error) {
|
|
141
190
|
const durationMs = Date.now() - startedAt;
|
|
191
|
+
const logContext = {
|
|
192
|
+
category: "request_processing",
|
|
193
|
+
outcome: "fail",
|
|
194
|
+
durationMs,
|
|
195
|
+
serviceName,
|
|
196
|
+
methodName,
|
|
197
|
+
userId: socket.userId,
|
|
198
|
+
socketId: socket.id,
|
|
199
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
200
|
+
};
|
|
201
|
+
if (this.logPayloads) {
|
|
202
|
+
logContext.payload = payload;
|
|
203
|
+
}
|
|
142
204
|
this.logger.error(
|
|
143
205
|
`User ${userIdShort} ${serviceName}.${methodName} -> fail`,
|
|
144
|
-
|
|
145
|
-
category: "request_processing",
|
|
146
|
-
outcome: "fail",
|
|
147
|
-
durationMs,
|
|
148
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
149
|
-
}
|
|
206
|
+
logContext
|
|
150
207
|
);
|
|
151
208
|
const errorResponse = {
|
|
152
209
|
success: false,
|
|
@@ -256,7 +313,10 @@ function createQuickdrawServer(options) {
|
|
|
256
313
|
credentials: options.cors?.credentials ?? true
|
|
257
314
|
}
|
|
258
315
|
});
|
|
259
|
-
const registry = new ServiceRegistry(io, {
|
|
316
|
+
const registry = new ServiceRegistry(io, {
|
|
317
|
+
logger,
|
|
318
|
+
methodLogging: options.methodLogging
|
|
319
|
+
});
|
|
260
320
|
io.use(async (socket, next) => {
|
|
261
321
|
const quickdrawSocket = socket;
|
|
262
322
|
try {
|
|
@@ -303,6 +363,34 @@ function createQuickdrawServer(options) {
|
|
|
303
363
|
`Registered services: ${registry.getServices().join(", ")}`
|
|
304
364
|
);
|
|
305
365
|
});
|
|
366
|
+
const shutdown = async (signal) => {
|
|
367
|
+
serverLogger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
368
|
+
httpServer.close(() => {
|
|
369
|
+
serverLogger.info("HTTP server closed");
|
|
370
|
+
});
|
|
371
|
+
const sockets = await io.fetchSockets();
|
|
372
|
+
serverLogger.info(`Closing ${sockets.length} active socket connections...`);
|
|
373
|
+
for (const socket of sockets) {
|
|
374
|
+
const quickdrawSocket = socket;
|
|
375
|
+
for (const service of registry.getServiceInstances()) {
|
|
376
|
+
try {
|
|
377
|
+
service.unsubscribeSocket(quickdrawSocket);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
socket.disconnect(true);
|
|
382
|
+
}
|
|
383
|
+
io.close(() => {
|
|
384
|
+
serverLogger.info("Socket.io server closed");
|
|
385
|
+
process.exit(0);
|
|
386
|
+
});
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
serverLogger.error("Graceful shutdown timeout, forcing exit");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}, 1e4);
|
|
391
|
+
};
|
|
392
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
393
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
306
394
|
return { io, httpServer, registry };
|
|
307
395
|
}
|
|
308
396
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":";;;;;;;;AA8KO,IAAM,aAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACpE,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,KAAK,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACrE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,OAAO,MAAM;AACf,CAAA;;;ACvJO,IAAM,kBAAN,MAAyD;AAAA,EAC7C,EAAA;AAAA,EACA,QAAA,uBAAiD,GAAA,EAAI;AAAA,EACrD,MAAA;AAAA,EAEjB,WAAA,CAAY,IAAoB,OAAA,EAA+B;AAC7D,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AACV,IAAA,IAAA,CAAK,MAAA,GAAS,OAAA,EAAS,MAAA,EAAQ,KAAA,CAAM,EAAE,OAAA,EAAS,iBAAA,EAAmB,CAAA,IACjE,aAAA,CAAc,KAAA,CAAM,EAAE,OAAA,EAAS,mBAAmB,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKO,eAAA,CACL,aACA,OAAA,EACM;AACN,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,WAAA,EAAa,OAAO,CAAA;AACtC,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,CAAA,oBAAA,EAAuB,WAAW,CAAA,CAAE,CAAA;AAGrD,IAAA,IAAA,CAAK,sBAAA,CAAuB,aAAa,OAAO,CAAA;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAA,CACN,aACA,OAAA,EACM;AACN,IAAA,MAAM,OAAA,GAAU,QAAQ,gBAAA,EAAiB;AAEzC,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,SAAA,GAAY,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI,OAAO,IAAI,CAAA,CAAA;AAC/C,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,CAAA,0BAAA,EAA6B,SAAS,CAAA,CAAE,CAAA;AAGzD,MAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,YAAA,EAAc,CAAC,MAAA,KAAW;AACnC,QAAA,IAAA,CAAK,sBAAA;AAAA,UACH,MAAA;AAAA,UACA,SAAA;AAAA,UACA,MAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF,CAAC,CAAA;AAAA,IACH;AAGA,IAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,YAAA,EAAc,CAAC,MAAA,KAAW;AACnC,MAAA,IAAA,CAAK,4BAAA;AAAA,QACH,MAAA;AAAA,QACA,GAAG,WAAW,CAAA,UAAA,CAAA;AAAA,QACd;AAAA,OACF;AACA,MAAA,IAAA,CAAK,8BAAA;AAAA,QACH,MAAA;AAAA,QACA,GAAG,WAAW,CAAA,YAAA,CAAA;AAAA,QACd;AAAA,OACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAA,CACN,MAAA,EACA,SAAA,EACA,MAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,OACE,SACA,QAAA,KACG;AACH,QAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,QAAA,MAAM,CAAC,WAAA,EAAa,UAAU,CAAA,GAAI,SAAA,CAAU,MAAM,GAAG,CAAA;AACrD,QAAA,MAAM,cAAc,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,IAAK,MAAA;AAElD,QAAA,IAAI;AAEF,UAAA,IAAI,CAAC,MAAA,CAAO,MAAA,IAAU,MAAA,CAAO,WAAW,QAAA,EAAU;AAChD,YAAA,MAAM,aAAA,GAA0C;AAAA,cAC9C,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,yBAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACR;AACA,YAAA,QAAA,GAAW,aAAa,CAAA;AACxB,YAAA;AAAA,UACF;AAGA,UAAA,IAAI,gBAAA,GAAmB,OAAA;AACvB,UAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,YAAA,MAAMA,OAAAA,GAAS,MAAA,CAAO,MAAA,CAAO,SAAA,CAAU,OAAO,CAAA;AAC9C,YAAA,IAAI,CAACA,QAAO,OAAA,EAAS;AACnB,cAAA,MAAM,aAAA,GAA0C;AAAA,gBAC9C,OAAA,EAAS,KAAA;AAAA,gBACT,KAAA,EAAO,CAAA,kBAAA,EAAqBA,OAAAA,CAAO,KAAA,CAAM,OAAO,CAAA,CAAA;AAAA,gBAChD,IAAA,EAAM;AAAA,eACR;AACA,cAAA,QAAA,GAAW,aAAa,CAAA;AACxB,cAAA;AAAA,YACF;AACA,YAAA,gBAAA,GAAmBA,OAAAA,CAAO,IAAA;AAAA,UAC5B;AAGA,UAAA,IAAI,OAAA;AACJ,UAAA,IAAI,OAAO,cAAA,EAAgB;AACzB,YAAA,OAAA,GAAU,MAAA,CAAO,cAAA,CAAe,gBAAgB,CAAA,IAAK,KAAA,CAAA;AAAA,UACvD,WACE,gBAAA,IACA,OAAO,gBAAA,KAAqB,QAAA,IAC5B,QAAQ,gBAAA,EACR;AACA,YAAA,MAAM,UAAW,gBAAA,CAA6C,EAAA;AAC9D,YAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,cAAA,OAAA,GAAU,OAAA;AAAA,YACZ;AAAA,UACF;AAGA,UAAA,MAAM,OAAA,CAAQ,qBAAA,CAAsB,MAAA,CAAO,MAAA,EAAQ,QAAQ,OAAO,CAAA;AAGlE,UAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,YACV,CAAA,KAAA,EAAQ,WAAW,CAAA,CAAA,EAAI,WAAW,IAAI,UAAU,CAAA,SAAA,CAAA;AAAA,YAChD;AAAA,cACE,QAAA,EAAU,oBAAA;AAAA,cACV,WAAA;AAAA,cACA,UAAA;AAAA,cACA,QAAQ,MAAA,CAAO,MAAA;AAAA,cACf,UAAU,MAAA,CAAO;AAAA;AACnB,WACF;AAGA,UAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,OAAA,CAAQ,gBAAA,EAAkB;AAAA,YACpD,QAAQ,MAAA,CAAO,MAAA;AAAA,YACf,UAAU,MAAA,CAAO,EAAA;AAAA,YACjB,aAAA,EAAe,MAAA,CAAO,aAAA,IAAiB;AAAC,WACzC,CAAA;AAED,UAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAChC,UAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,YACV,CAAA,KAAA,EAAQ,WAAW,CAAA,CAAA,EAAI,WAAW,IAAI,UAAU,CAAA,WAAA,CAAA;AAAA,YAChD;AAAA,cACE,QAAA,EAAU,oBAAA;AAAA,cACV,OAAA,EAAS,SAAA;AAAA,cACT,UAAA;AAAA,cACA,WAAA;AAAA,cACA,UAAA;AAAA,cACA,QAAQ,MAAA,CAAO;AAAA;AACjB,WACF;AAEA,UAAA,MAAM,eAAA,GAA4C;AAAA,YAChD,OAAA,EAAS,IAAA;AAAA,YACT,IAAA,EAAM;AAAA,WACR;AACA,UAAA,QAAA,GAAW,eAAe,CAAA;AAAA,QAC5B,SAAS,KAAA,EAAO;AACd,UAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAChC,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,YACV,CAAA,KAAA,EAAQ,WAAW,CAAA,CAAA,EAAI,WAAW,IAAI,UAAU,CAAA,QAAA,CAAA;AAAA,YAChD;AAAA,cACE,QAAA,EAAU,oBAAA;AAAA,cACV,OAAA,EAAS,MAAA;AAAA,cACT,UAAA;AAAA,cACA,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA;AAClD,WACF;AAEA,UAAA,MAAM,aAAA,GAA0C;AAAA,YAC9C,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,gBAAA;AAAA,YAChD,IAAA,EAAM;AAAA,WACR;AACA,UAAA,QAAA,GAAW,aAAa,CAAA;AAAA,QAC1B;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,4BAAA,CACN,MAAA,EACA,SAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,OACE,SACA,QAAA,KACG;AACH,QAAA,IAAI;AACF,UAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,yBAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,SAAA;AAAA,YACzB,OAAA,CAAQ,OAAA;AAAA,YACR,MAAA;AAAA,YACC,QAAQ,aAAA,IAAmD;AAAA,WAC9D;AAEA,UAAA,IAAI,SAAS,IAAA,EAAM;AACjB,YAAA,QAAA,GAAW;AAAA,cACT,OAAA,EAAS,KAAA;AAAA,cACT,KAAA,EAAO,kCAAA;AAAA,cACP,IAAA,EAAM;AAAA,aACP,CAAA;AACD,YAAA;AAAA,UACF;AAEA,UAAA,QAAA,GAAW,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,QACpC,SAAS,KAAA,EAAO;AACd,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,sBAAA,EAAyB,SAAS,CAAA,CAAA,CAAA,EAAK;AAAA,YACvD,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AACD,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,8BAAA,CACN,MAAA,EACA,SAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,SAAA;AAAA,MACA,CACE,SACA,QAAA,KAGG;AACH,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,WAAA,CAAY,OAAA,CAAQ,OAAA,EAAS,MAAM,CAAA;AAC3C,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,IAAA;AAAA,YACT,MAAM,EAAE,YAAA,EAAc,IAAA,EAAM,OAAA,EAAS,QAAQ,OAAA;AAAQ,WACtD,CAAA;AAAA,QACH,SAAS,KAAA,EAAO;AACd,UAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,wBAAA,EAA2B,SAAS,CAAA,CAAA,CAAA,EAAK;AAAA,YACzD,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AACD,UAAA,QAAA,GAAW;AAAA,YACT,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA,WACjD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,WAAA,GAAwB;AAC7B,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKO,mBAAA,GAA6C;AAClD,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA;AAAA,EAC1C;AACF,CAAA;;;AC3RO,SAAS,sBACd,OAAA,EACuB;AACvB,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,aAAA;AACjC,EAAA,MAAM,eAAe,MAAA,CAAO,KAAA,CAAM,EAAE,OAAA,EAAS,mBAAmB,CAAA;AAGhE,EAAA,MAAM,MAAM,OAAA,EAAQ;AACpB,EAAA,GAAA,CAAI,GAAA,CAAI,OAAA,CAAQ,IAAA,EAAM,CAAA;AAGtB,EAAA,GAAA,CAAI,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AAChC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAA,qBAAe,IAAA,EAAK,EAAE,WAAA,EAAY,EAAG,CAAA;AAAA,EAChE,CAAC,CAAA;AAGD,EAAA,MAAM,UAAA,GAAa,aAAa,GAAG,CAAA;AAGnC,EAAA,MAAM,EAAA,GAAK,IAAIC,MAAA,CAAe,UAAA,EAAY;AAAA,IACxC,IAAA,EAAM;AAAA,MACJ,MAAA,EAAQ,OAAA,CAAQ,IAAA,EAAM,MAAA,IAAU,GAAA;AAAA,MAChC,SAAS,OAAA,CAAQ,IAAA,EAAM,OAAA,IAAW,CAAC,OAAO,MAAM,CAAA;AAAA,MAChD,WAAA,EAAa,OAAA,CAAQ,IAAA,EAAM,WAAA,IAAe;AAAA;AAC5C,GACD,CAAA;AAGD,EAAA,MAAM,WAAW,IAAI,eAAA,CAAgB,EAAA,EAAI,EAAE,QAAQ,CAAA;AAGnD,EAAA,EAAA,CAAG,GAAA,CAAI,OAAO,MAAA,EAAQ,IAAA,KAAS;AAC7B,IAAA,MAAM,eAAA,GAAkB,MAAA;AAExB,IAAA,IAAI;AACF,MAAA,IAAI,OAAA,CAAQ,MAAM,YAAA,EAAc;AAC9B,QAAA,MAAM,IAAA,GAAO,OAAO,SAAA,CAAU,IAAA;AAC9B,QAAA,MAAM,SAAS,MAAM,OAAA,CAAQ,IAAA,CAAK,YAAA,CAAa,iBAAiB,IAAI,CAAA;AACpE,QAAA,eAAA,CAAgB,MAAA,GAAS,MAAA;AAIzB,QAAA,eAAA,CAAgB,gBAAgB,EAAC;AAAA,MACnC;AACA,MAAA,IAAA,EAAK;AAAA,IACP,SAAS,KAAA,EAAO;AACd,MAAA,YAAA,CAAa,MAAM,uBAAA,EAAyB;AAAA,QAC1C,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AAAA,QAChD,UAAU,MAAA,CAAO;AAAA,OAClB,CAAA;AACD,MAAA,IAAA,CAAK,IAAI,KAAA,CAAM,uBAAuB,CAAC,CAAA;AAAA,IACzC;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,KAAA,MAAW,CAAC,aAAa,OAAO,CAAA,IAAK,OAAO,OAAA,CAAQ,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACrE,IAAA,QAAA,CAAS,eAAA,CAAgB,aAAa,OAAO,CAAA;AAAA,EAC/C;AAGA,EAAA,EAAA,CAAG,EAAA,CAAG,YAAA,EAAc,CAAC,MAAA,KAAW;AAC9B,IAAA,MAAM,eAAA,GAAkB,MAAA;AACxB,IAAA,YAAA,CAAa,KAAK,kBAAA,EAAoB;AAAA,MACpC,UAAU,eAAA,CAAgB,EAAA;AAAA,MAC1B,QAAQ,eAAA,CAAgB;AAAA,KACzB,CAAA;AAED,IAAA,eAAA,CAAgB,EAAA,CAAG,cAAc,MAAM;AACrC,MAAA,YAAA,CAAa,KAAK,qBAAA,EAAuB;AAAA,QACvC,UAAU,eAAA,CAAgB,EAAA;AAAA,QAC1B,QAAQ,eAAA,CAAgB;AAAA,OACzB,CAAA;AAGD,MAAA,KAAA,MAAW,OAAA,IAAW,QAAA,CAAS,mBAAA,EAAoB,EAAG;AACpD,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,kBAAkB,eAAe,CAAA;AAAA,QAC3C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AAGD,EAAA,UAAA,CAAW,MAAA,CAAO,OAAA,CAAQ,IAAA,EAAM,SAAA,EAAW,MAAM;AAC/C,IAAA,YAAA,CAAa,IAAA,CAAK,CAAA,yBAAA,EAA4B,OAAA,CAAQ,IAAI,CAAA,CAAE,CAAA;AAC5D,IAAA,YAAA,CAAa,IAAA;AAAA,MACX,wBAAwB,QAAA,CAAS,WAAA,EAAY,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,KAC3D;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,EAAE,EAAA,EAAI,UAAA,EAAY,QAAA,EAAS;AACpC;;;AC9FA,IAAI,QAAA,GAAW,GAAA;AAKR,SAAS,gBAAA,GAA2B;AACzC,EAAA,OAAO,QAAA,EAAA;AACT;AAsBA,eAAsB,iBACpB,OAAA,EACqB;AACrB,EAAA,MAAM,OAAO,gBAAA,EAAiB;AAE9B,EAAA,MAAM,SAAS,qBAAA,CAAsB;AAAA,IACnC,IAAA;AAAA,IACA,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,IAAA,EAAM;AAAA;AAAA,MAEJ,YAAA,EAAc,OAAO,OAAA,EAAS,IAAA,KAAS;AACrC,QAAA,OAAO,IAAA,CAAK,MAAA;AAAA,MACd;AAAA;AACF,GACD,CAAA;AAGD,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,IAAA,MAAA,CAAO,UAAA,CAAW,IAAA,CAAK,WAAA,EAAa,OAAO,CAAA;AAAA,EAC7C,CAAC,CAAA;AAGD,EAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,IAAA,MAAM,QAAQ,MAAA,EAAO;AAAA,EACvB;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA,EAAM,MACJ,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AAC7B,MAAA,MAAA,CAAO,GAAG,KAAA,EAAM;AAChB,MAAA,MAAA,CAAO,UAAA,CAAW,KAAA,CAAM,MAAM,OAAA,EAAS,CAAA;AAAA,IACzC,CAAC,CAAA;AAAA,IACH,SAAA,EAAW,CAAC,MAAA,KAAmB,aAAA,CAAc,MAAM,MAAM;AAAA,GAC3D;AACF;AAKA,eAAsB,aAAA,CACpB,MACA,MAAA,EACqB;AACrB,EAAA,MAAM,MAAA,GAASC,EAAA,CAAS,CAAA,iBAAA,EAAoB,IAAI,CAAA,CAAA,EAAI;AAAA,IAClD,IAAA,EAAM,EAAE,MAAA,EAAO;AAAA,IACf,UAAA,EAAY,CAAC,WAAW,CAAA;AAAA,IACxB,WAAA,EAAa;AAAA,GACd,CAAA;AAED,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,oBAAoB,CAAC,CAAA;AAAA,IACxC,GAAG,GAAI,CAAA;AAEP,IAAA,MAAA,CAAO,EAAA,CAAG,WAAW,MAAM;AACzB,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,OAAA,EAAQ;AAAA,IACV,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,eAAA,EAAiB,CAAC,GAAA,KAAQ;AAClC,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,MAAA,CAAO,GAAG,CAAA;AAAA,IACZ,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,MAAM,CAAsB,KAAA,EAAe,YACzC,WAAA,CAAiC,MAAA,EAAQ,OAAO,OAAO,CAAA;AAAA,IACzD,KAAA,EAAO,MAAM,MAAA,CAAO,KAAA;AAAM,GAC5B;AACF;AAKO,SAAS,WAAA,CACd,MAAA,EACA,KAAA,EACA,OAAA,EACA,YAAoB,GAAA,EACA;AACpB,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,KAAK,EAAE,CAAC,CAAA;AAAA,IAClD,GAAG,SAAS,CAAA;AAEZ,IAAA,MAAA,CAAO,IAAA,CAAK,KAAA,EAAO,OAAA,EAAS,CAAC,QAAA,KAAyC;AACpE,MAAA,YAAA,CAAa,OAAO,CAAA;AAEpB,MAAA,IAAI,SAAS,OAAA,EAAS;AACpB,QAAA,OAAA,CAAQ,SAAS,IAAI,CAAA;AAAA,MACvB,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,MAClC;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAKO,SAAS,YAAA,CACd,MAAA,EACA,KAAA,EACA,SAAA,GAAoB,GAAA,EACR;AACZ,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC/B,MAAA,MAAA,CAAO,GAAA,CAAI,OAAO,OAAO,CAAA;AACzB,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,EAAE,CAAC,CAAA;AAAA,IACxD,GAAG,SAAS,CAAA;AAEZ,IAAA,MAAM,OAAA,GAAU,CAAC,IAAA,KAAY;AAC3B,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,MAAA,CAAO,GAAA,CAAI,OAAO,OAAO,CAAA;AACzB,MAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,IACd,CAAA;AAEA,IAAA,MAAA,CAAO,EAAA,CAAG,OAAO,OAAO,CAAA;AAAA,EAC1B,CAAC,CAAA;AACH","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<\n TPayload = unknown,\n TResponse = unknown,\n> {\n name: string;\n access: AccessLevel;\n handler: (\n payload: TPayload,\n context: ServiceMethodContext\n ) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<\n T extends Record<string, { payload: unknown; response: unknown }>,\n> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type {\n ServiceResponse,\n ServiceMethodDefinition,\n Logger,\n} from \"../shared/types\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n BaseServiceInstance,\n ServiceRegistryInstance,\n} from \"./types\";\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n\n constructor(io: SocketIOServer, options?: { logger?: Logger }) {\n this.io = io;\n this.logger = options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n this.services.set(serviceName, service);\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and wire public methods\n this.discoverServiceMethods(serviceName, service);\n }\n\n /**\n * Discover and register all public methods from a service.\n */\n private discoverServiceMethods(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Register on connection\n this.io.on(\"connection\", (socket) => {\n this.registerMethodListener(\n socket as QuickdrawSocket,\n eventName,\n method,\n service\n );\n });\n }\n\n // Register subscription handlers\n this.io.on(\"connection\", (socket) => {\n this.registerSubscriptionListener(\n socket as QuickdrawSocket,\n `${serviceName}:subscribe`,\n service\n );\n this.registerUnsubscriptionListener(\n socket as QuickdrawSocket,\n `${serviceName}:unsubscribe`,\n service\n );\n });\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: unknown,\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n }\n );\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n const durationMs = Date.now() - startedAt;\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n }\n );\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n const durationMs = Date.now() - startedAt;\n this.logger.error(\n `User ${userIdShort} ${serviceName}.${methodName} -> fail`,\n {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n error: error instanceof Error ? error.message : \"Unknown error\",\n }\n );\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n }\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\"\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (\n response: ServiceResponse<{ unsubscribed: true; entryId: string }>\n ) => void\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n QuickdrawServerOptions,\n QuickdrawServerResult,\n} from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(\n options: QuickdrawServerOptions\n): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry\n const registry = new ServiceRegistry(io, { logger });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(\n `Registered services: ${registry.getServices().join(\", \")}`\n );\n });\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(\n event: string,\n payload: TPayload\n ) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(\n options: CreateTestServerOptions\n): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(\n port: number,\n userId: string\n): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":"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","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<\n TPayload = unknown,\n TResponse = unknown,\n> {\n name: string;\n access: AccessLevel;\n handler: (\n payload: TPayload,\n context: ServiceMethodContext\n ) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<\n T extends Record<string, { payload: unknown; response: unknown }>,\n> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type {\n ServiceResponse,\n ServiceMethodDefinition,\n Logger,\n} from \"../shared/types\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n BaseServiceInstance,\n ServiceRegistryInstance,\n} from \"./types\";\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n * - Uses a single connection handler for all services (memory efficient)\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport interface ServiceRegistryOptions {\n logger?: Logger;\n /**\n * Automatic method logging configuration.\n * @default { enabled: true, logPayloads: false, logResponses: false }\n */\n methodLogging?: {\n enabled?: boolean;\n logPayloads?: boolean;\n logResponses?: boolean;\n };\n}\n\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n private readonly methodLoggingEnabled: boolean;\n private readonly logPayloads: boolean;\n private readonly logResponses: boolean;\n private connectionHandlerInstalled = false;\n\n // Store method metadata for deferred registration\n private readonly methodRegistry: Map<\n string,\n {\n serviceName: string;\n method: ServiceMethodDefinition<unknown, unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n constructor(io: SocketIOServer, options?: ServiceRegistryOptions) {\n this.io = io;\n this.logger =\n options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n \n // Method logging configuration (enabled by default)\n this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;\n this.logPayloads = options?.methodLogging?.logPayloads ?? false;\n this.logResponses = options?.methodLogging?.logResponses ?? false;\n\n if (this.methodLoggingEnabled) {\n this.logger.info(\"Automatic method logging enabled\", {\n logPayloads: this.logPayloads,\n logResponses: this.logResponses,\n });\n }\n }\n\n /**\n * Install the single connection handler that wires all registered services.\n * This is called lazily on first service registration.\n */\n private installConnectionHandler(): void {\n if (this.connectionHandlerInstalled) return;\n this.connectionHandlerInstalled = true;\n\n this.io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n this.setupSocketListeners(quickdrawSocket);\n });\n\n this.logger.info(\"Installed single connection handler for all services\");\n }\n\n /**\n * Set up all event listeners for a newly connected socket.\n */\n private setupSocketListeners(socket: QuickdrawSocket): void {\n // Register method listeners for all registered services\n for (const [eventName, { method, service }] of this.methodRegistry) {\n this.registerMethodListener(socket, eventName, method, service);\n }\n\n // Register subscription/unsubscription listeners for all services\n for (const [serviceName, service] of this.services) {\n this.registerSubscriptionListener(\n socket,\n `${serviceName}:subscribe`,\n service\n );\n this.registerUnsubscriptionListener(\n socket,\n `${serviceName}:unsubscribe`,\n service\n );\n }\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n this.services.set(serviceName, service);\n\n // Pass io instance to service for room-based broadcasts\n if (service.setIo) {\n service.setIo(this.io);\n }\n\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and store method metadata\n this.discoverServiceMethods(serviceName, service);\n\n // Ensure connection handler is installed\n this.installConnectionHandler();\n }\n\n /**\n * Discover and store all public methods from a service.\n */\n private discoverServiceMethods(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Store method metadata for deferred registration on connection\n this.methodRegistry.set(eventName, {\n serviceName,\n method,\n service,\n });\n }\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: unknown,\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start (if enabled)\n if (this.methodLoggingEnabled) {\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n };\n if (this.logPayloads) {\n logContext.payload = validatedPayload;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n logContext\n );\n }\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n // Log success (if enabled)\n if (this.methodLoggingEnabled) {\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n };\n if (this.logResponses) {\n logContext.response = result;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n logContext\n );\n }\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n // Always log errors, even if methodLogging is disabled\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n if (this.logPayloads) {\n logContext.payload = payload;\n }\n this.logger.error(\n `User ${userIdShort} ${serviceName}.${methodName} -> fail`,\n logContext\n );\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n }\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\"\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error:\n error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (\n response: ServiceResponse<{ unsubscribed: true; entryId: string }>\n ) => void\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error:\n error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n QuickdrawServerOptions,\n QuickdrawServerResult,\n} from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(\n options: QuickdrawServerOptions\n): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry with method logging configuration\n const registry = new ServiceRegistry(io, { \n logger,\n methodLogging: options.methodLogging,\n });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(\n `Registered services: ${registry.getServices().join(\", \")}`\n );\n });\n\n // Graceful shutdown handler\n const shutdown = async (signal: string) => {\n serverLogger.info(`Received ${signal}, starting graceful shutdown...`);\n\n // Stop accepting new connections\n httpServer.close(() => {\n serverLogger.info(\"HTTP server closed\");\n });\n\n // Close all Socket.io connections gracefully\n const sockets = await io.fetchSockets();\n serverLogger.info(`Closing ${sockets.length} active socket connections...`);\n\n for (const socket of sockets) {\n const quickdrawSocket = socket as unknown as QuickdrawSocket;\n \n // Unsubscribe from all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n\n // Disconnect socket\n socket.disconnect(true);\n }\n\n // Close Socket.io server\n io.close(() => {\n serverLogger.info(\"Socket.io server closed\");\n process.exit(0);\n });\n\n // Force exit after timeout\n setTimeout(() => {\n serverLogger.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, 10000); // 10 second timeout\n };\n\n // Register shutdown handlers\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(\n event: string,\n payload: TPayload\n ) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(\n options: CreateTestServerOptions\n): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(\n port: number,\n userId: string\n): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -6,6 +6,68 @@ type ACE = {
|
|
|
6
6
|
level: AccessLevel;
|
|
7
7
|
};
|
|
8
8
|
type ACL = ACE[];
|
|
9
|
+
/**
|
|
10
|
+
* Interface for entities that support entry-level ACL stored as JSON.
|
|
11
|
+
*
|
|
12
|
+
* Use this pattern when:
|
|
13
|
+
* - You want simple ACL without a separate membership table
|
|
14
|
+
* - Access patterns are straightforward (read/write permissions)
|
|
15
|
+
* - You don't need to query "all entities user X can access" efficiently
|
|
16
|
+
*
|
|
17
|
+
* The default `checkEntryACL` in BaseService expects entities to have this shape.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```prisma
|
|
21
|
+
* model Document {
|
|
22
|
+
* id String @id @default(cuid())
|
|
23
|
+
* title String
|
|
24
|
+
* acl Json? // Stores ACL array
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* class DocumentService extends BaseService<Document & ACLEntity, ...> {
|
|
31
|
+
* constructor(prisma: PrismaClient) {
|
|
32
|
+
* super({ serviceName: 'documentService', hasEntryACL: true });
|
|
33
|
+
* this.setDelegate(prisma.document);
|
|
34
|
+
* // Default checkEntryACL will work automatically
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
interface ACLEntity {
|
|
40
|
+
id: string;
|
|
41
|
+
acl?: ACL | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Interface for users with service-level access control.
|
|
45
|
+
*
|
|
46
|
+
* The `serviceAccess` field maps service names to access levels, granting
|
|
47
|
+
* blanket permissions across all entries in that service.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```prisma
|
|
51
|
+
* model User {
|
|
52
|
+
* id String @id @default(cuid())
|
|
53
|
+
* serviceAccess Json? // { "chatService": "Admin", "documentService": "Read" }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // User with Admin access to chatService can perform any operation
|
|
60
|
+
* // on any chat, regardless of entry-level ACL
|
|
61
|
+
* const user: QuickdrawUser = {
|
|
62
|
+
* id: "user-123",
|
|
63
|
+
* serviceAccess: { chatService: "Admin" }
|
|
64
|
+
* };
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
interface QuickdrawUser {
|
|
68
|
+
id: string;
|
|
69
|
+
serviceAccess?: Record<string, AccessLevel> | null;
|
|
70
|
+
}
|
|
9
71
|
type ServiceResponse<T = unknown> = {
|
|
10
72
|
success: true;
|
|
11
73
|
data: T;
|
|
@@ -121,6 +183,54 @@ type AdminUnsubscribeAllResponse = {
|
|
|
121
183
|
id: string;
|
|
122
184
|
unsubscribed: number;
|
|
123
185
|
};
|
|
186
|
+
/**
|
|
187
|
+
* Supported field types for admin UI generation.
|
|
188
|
+
*/
|
|
189
|
+
type AdminFieldType = "string" | "number" | "boolean" | "date" | "enum" | "json" | "relation";
|
|
190
|
+
/**
|
|
191
|
+
* Configuration for a single field in the admin UI.
|
|
192
|
+
* Derived from Zod schemas with optional overrides.
|
|
193
|
+
*/
|
|
194
|
+
interface AdminFieldConfig {
|
|
195
|
+
/** Field name (property key on the entity) */
|
|
196
|
+
name: string;
|
|
197
|
+
/** Field type for rendering appropriate input */
|
|
198
|
+
type: AdminFieldType;
|
|
199
|
+
/** Human-readable label */
|
|
200
|
+
label: string;
|
|
201
|
+
/** Whether the field is required for creation */
|
|
202
|
+
required: boolean;
|
|
203
|
+
/** Whether the field can be edited (false for id, createdAt, etc.) */
|
|
204
|
+
editable: boolean;
|
|
205
|
+
/** Whether to show this field as a table column */
|
|
206
|
+
showInTable: boolean;
|
|
207
|
+
/** Whether the table can be sorted by this field */
|
|
208
|
+
sortable: boolean;
|
|
209
|
+
/** For enum fields, the allowed values */
|
|
210
|
+
enumValues?: string[];
|
|
211
|
+
/** For relation fields, the related service name */
|
|
212
|
+
relationService?: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Service metadata for admin UI generation.
|
|
216
|
+
* Returned by the adminMeta method.
|
|
217
|
+
*/
|
|
218
|
+
interface AdminServiceMeta {
|
|
219
|
+
/** Internal service name (e.g., "chatService") */
|
|
220
|
+
serviceName: string;
|
|
221
|
+
/** Human-readable display name (e.g., "Chats") */
|
|
222
|
+
displayName: string;
|
|
223
|
+
/** Field configurations derived from schema */
|
|
224
|
+
fields: AdminFieldConfig[];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Payload for adminMeta method (no parameters needed)
|
|
228
|
+
*/
|
|
229
|
+
type AdminMetaPayload = Record<string, never>;
|
|
230
|
+
/**
|
|
231
|
+
* Response type for adminMeta method
|
|
232
|
+
*/
|
|
233
|
+
type AdminMetaResponse = AdminServiceMeta;
|
|
124
234
|
type SubscribePayload = {
|
|
125
235
|
entryId: string;
|
|
126
236
|
requiredLevel?: AccessLevel;
|
|
@@ -155,4 +265,4 @@ interface Logger {
|
|
|
155
265
|
*/
|
|
156
266
|
declare const consoleLogger: Logger;
|
|
157
267
|
|
|
158
|
-
export { type ACE, type ACL, type AccessLevel, type AdminCreatePayload, type AdminDeletePayload, type AdminDeleteResponse, type AdminGetPayload, type AdminGetSubscribersPayload, type AdminGetSubscribersResponse, type AdminListPayload, type AdminListResponse, type AdminReemitPayload, type AdminReemitResponse, type AdminSetEntryACLPayload, type AdminUnsubscribeAllPayload, type AdminUnsubscribeAllResponse, type AdminUpdatePayload, type ExtractPayload, type ExtractResponse, type Logger, type ServiceMethodContext, type ServiceMethodDefinition, type ServiceMethodMap, type ServiceResponse, type SubscribePayload, type UnsubscribePayload, consoleLogger };
|
|
268
|
+
export { type ACE, type ACL, type ACLEntity, type AccessLevel, type AdminCreatePayload, type AdminDeletePayload, type AdminDeleteResponse, type AdminFieldConfig, type AdminFieldType, type AdminGetPayload, type AdminGetSubscribersPayload, type AdminGetSubscribersResponse, type AdminListPayload, type AdminListResponse, type AdminMetaPayload, type AdminMetaResponse, type AdminReemitPayload, type AdminReemitResponse, type AdminServiceMeta, type AdminSetEntryACLPayload, type AdminUnsubscribeAllPayload, type AdminUnsubscribeAllResponse, type AdminUpdatePayload, type ExtractPayload, type ExtractResponse, type Logger, type QuickdrawUser, type ServiceMethodContext, type ServiceMethodDefinition, type ServiceMethodMap, type ServiceResponse, type SubscribePayload, type UnsubscribePayload, consoleLogger };
|
package/dist/shared/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/types.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/shared/types.ts"],"names":[],"mappings":";AAkTO,IAAM,aAAA,GAAwB;AAAA,EACnC,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,IAAI,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACpE,IAAA,EAAM,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,KAAK,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACrE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,KAAA,EAAO,CAAC,OAAA,EAAS,IAAA,KAAS,OAAA,CAAQ,MAAM,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,IAAA,IAAQ,EAAE,CAAA;AAAA,EACxE,OAAO,MAAM;AACf","file":"index.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<\n TPayload = unknown,\n TResponse = unknown,\n> {\n name: string;\n access: AccessLevel;\n handler: (\n payload: TPayload,\n context: ServiceMethodContext\n ) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<\n T extends Record<string, { payload: unknown; response: unknown }>,\n> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n"]}
|