@arcote.tech/arc-host 0.1.5 → 0.1.6

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/host.d.ts CHANGED
@@ -33,6 +33,7 @@ declare class RTCHost implements RealTimeCommunicationAdapter {
33
33
  private setNestedProperty;
34
34
  private handleCommand;
35
35
  private handleQuery;
36
+ private handleRoute;
36
37
  private setupServer;
37
38
  private isPublicEndpoint;
38
39
  private handleSync;
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../host.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAGvB,KAAK,4BAA4B,EAClC,MAAM,kBAAkB,CAAC;AAI1B,cAAM,OAAQ,YAAW,4BAA4B;IAMjD,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,KAAK,CAAuB;gBAG1B,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IASrC,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAI5C,IAAI,CACR,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,IAAI,GACL,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;YAIF,eAAe;IAS7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;YAwCX,aAAa;YAoDb,WAAW;IAiEzB,OAAO,CAAC,WAAW;IA6DnB,OAAO,CAAC,gBAAgB;YAsBV,UAAU;YA+CV,SAAS;IAevB,OAAO,CAAC,cAAc;CAGvB;AAED,eAAO,MAAM,cAAc,YACf,aAAa,aAAa,OAAO,CAAC,eAAe,CAAC,kBAE3D,CAAC"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../host.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAGvB,KAAK,4BAA4B,EAClC,MAAM,kBAAkB,CAAC;AAI1B,cAAM,OAAQ,YAAW,4BAA4B;IAMjD,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,KAAK,CAAuB;gBAG1B,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IASrC,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAI5C,IAAI,CACR,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,IAAI,GACL,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;YAIF,eAAe;IAS7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;YAwCX,aAAa;YAoDb,WAAW;YAiEX,WAAW;IAgEzB,OAAO,CAAC,WAAW;IAmEnB,OAAO,CAAC,gBAAgB;YA4BV,UAAU;YA+CV,SAAS;IAevB,OAAO,CAAC,cAAc;CAGvB;AAED,eAAO,MAAM,cAAc,YACf,aAAa,aAAa,OAAO,CAAC,eAAe,CAAC,kBAE3D,CAAC"}
package/dist/index.js CHANGED
@@ -4904,6 +4904,7 @@ var constructorString = Object.prototype.constructor.toString();
4904
4904
  class ArcContextElement {
4905
4905
  $event;
4906
4906
  name;
4907
+ queryBuilder;
4907
4908
  commandContext;
4908
4909
  commandClient;
4909
4910
  observer;
@@ -5632,7 +5633,7 @@ class ArcFindQuery extends ArcCollectionQuery {
5632
5633
  }
5633
5634
  onChange(change) {
5634
5635
  const lastResult = this.lastResult;
5635
- const lastResultAsArray = lastResult?.toArray() || [];
5636
+ const lastResultAsArray = lastResult || [];
5636
5637
  const index = lastResultAsArray.findIndex((e) => e._id === (change.type === "delete" ? change.id : change.id));
5637
5638
  const isInLastResult = index !== -1;
5638
5639
  const shouldBeInTheResult = change.type !== "delete" && this.checkItem(change.item);
@@ -6654,6 +6655,40 @@ class Model extends ModelBase {
6654
6655
  }
6655
6656
  });
6656
6657
  }
6658
+ routes(authContext) {
6659
+ return new Proxy({}, {
6660
+ get: (_, name) => {
6661
+ const element3 = this.context.elements.find((element4) => element4.name === name);
6662
+ if (!element3) {
6663
+ throw new Error(`Route element "${String(name)}" not found`);
6664
+ }
6665
+ if (typeof element3.getHandler !== "function") {
6666
+ throw new Error(`Element "${String(name)}" does not have route handlers`);
6667
+ }
6668
+ return async (method, req, routeParams, url) => {
6669
+ const handler = element3.getHandler(method);
6670
+ if (!handler) {
6671
+ throw new Error(`Method ${method} not allowed for route ${String(name)}`);
6672
+ }
6673
+ const forkedDataStorage = this.dataStorage.fork();
6674
+ const eventPublisher = new EventPublisher(this.context, this.dataStorage, authContext);
6675
+ const publishEvent = async (event3) => {
6676
+ await eventPublisher.publishEvent(event3, forkedDataStorage);
6677
+ };
6678
+ const commandContext = this.context.commandContext(forkedDataStorage, publishEvent, authContext);
6679
+ try {
6680
+ const result = await handler(commandContext, req, routeParams, url);
6681
+ await forkedDataStorage.merge();
6682
+ eventPublisher.runAsyncListeners();
6683
+ return result;
6684
+ } catch (error) {
6685
+ this.catchErrorCallback(error);
6686
+ throw error;
6687
+ }
6688
+ };
6689
+ }
6690
+ });
6691
+ }
6657
6692
  get $debug() {
6658
6693
  return {};
6659
6694
  }
@@ -6746,7 +6781,7 @@ class RTCHost {
6746
6781
  }
6747
6782
  getDefaultAuthContext(ipAddress) {
6748
6783
  return {
6749
- userId: "anonymous",
6784
+ userId: undefined,
6750
6785
  roles: [],
6751
6786
  ipAddress
6752
6787
  };
@@ -6874,6 +6909,50 @@ class RTCHost {
6874
6909
  });
6875
6910
  }
6876
6911
  }
6912
+ async handleRoute(req) {
6913
+ const url = new URL(req.url);
6914
+ const method = req.method;
6915
+ let matchedRoute = null;
6916
+ let routeParams = {};
6917
+ for (const element of this.context.elements) {
6918
+ if (typeof element.matchesRoutePath === "function") {
6919
+ const { matches, params } = element.matchesRoutePath(url.pathname);
6920
+ if (matches) {
6921
+ matchedRoute = element;
6922
+ routeParams = params || {};
6923
+ break;
6924
+ }
6925
+ }
6926
+ }
6927
+ if (!matchedRoute) {
6928
+ return new Response("Route not found", { status: 404 });
6929
+ }
6930
+ const handler = matchedRoute.getHandler(method);
6931
+ if (!handler) {
6932
+ return new Response(`Method ${method} not allowed`, { status: 405 });
6933
+ }
6934
+ try {
6935
+ const authHeader = req.headers.get("Authorization");
6936
+ const token = authHeader?.replace("Bearer ", "");
6937
+ const clientIp = this.getClientIpAddress(req);
6938
+ let authContext;
6939
+ if (token && !matchedRoute.isPublic) {
6940
+ const payload = await this.verifyAuthToken(token);
6941
+ if (!payload) {
6942
+ return new Response("Invalid or expired token", { status: 401 });
6943
+ }
6944
+ authContext = this.tokenToAuthContext(payload, clientIp);
6945
+ } else {
6946
+ authContext = this.getDefaultAuthContext(clientIp);
6947
+ }
6948
+ const routes = this.model.routes(authContext);
6949
+ const response = await routes[matchedRoute.name](method, req, routeParams, url);
6950
+ return response;
6951
+ } catch (error) {
6952
+ console.error(`Error executing route ${matchedRoute.name}:`, error);
6953
+ return new Response("Internal Server Error", { status: 500 });
6954
+ }
6955
+ }
6877
6956
  setupServer() {
6878
6957
  this.server = Bun.serve({
6879
6958
  fetch: async (req, server) => {
@@ -6904,6 +6983,10 @@ class RTCHost {
6904
6983
  if (url.pathname === "/query" && req.method === "POST") {
6905
6984
  return await this.handleQuery(req);
6906
6985
  }
6986
+ const routeResponse = await this.handleRoute(req);
6987
+ if (routeResponse.status !== 404) {
6988
+ return routeResponse;
6989
+ }
6907
6990
  return new Response("Not Found", { status: 404 });
6908
6991
  },
6909
6992
  websocket: {
@@ -6919,15 +7002,21 @@ class RTCHost {
6919
7002
  });
6920
7003
  }
6921
7004
  isPublicEndpoint(pathname) {
6922
- if (!pathname.startsWith("/command/")) {
6923
- return false;
6924
- }
6925
- const commandName = pathname.split("/command/")[1];
6926
- if (!commandName) {
6927
- return false;
7005
+ for (const element of this.context.elements) {
7006
+ if (typeof element.matchesCommandPath === "function") {
7007
+ const { matches, isPublic } = element.matchesCommandPath(pathname);
7008
+ if (matches) {
7009
+ return isPublic;
7010
+ }
7011
+ }
7012
+ if (typeof element.matchesRoutePath === "function") {
7013
+ const { matches, isPublic } = element.matchesRoutePath(pathname);
7014
+ if (matches) {
7015
+ return isPublic;
7016
+ }
7017
+ }
6928
7018
  }
6929
- const contextElement = this.context.elements.find((element) => element.name === commandName);
6930
- return contextElement && "isPublic" in contextElement ? contextElement.isPublic === true : false;
7019
+ return false;
6931
7020
  }
6932
7021
  async handleSync(lastDate) {
6933
7022
  const syncDate = new Date;
package/host.ts CHANGED
@@ -102,7 +102,7 @@ class RTCHost implements RealTimeCommunicationAdapter {
102
102
  */
103
103
  private getDefaultAuthContext(ipAddress?: string): AuthContext {
104
104
  return {
105
- userId: "anonymous",
105
+ userId: undefined as any,
106
106
  roles: [],
107
107
  ipAddress,
108
108
  };
@@ -282,6 +282,70 @@ class RTCHost implements RealTimeCommunicationAdapter {
282
282
  }
283
283
  }
284
284
 
285
+ private async handleRoute(req: Request) {
286
+ const url = new URL(req.url);
287
+ const method = req.method;
288
+
289
+ // Find matching route
290
+ let matchedRoute: any = null;
291
+ let routeParams: Record<string, string> = {};
292
+
293
+ for (const element of this.context.elements) {
294
+ // Check if element has matchesRoutePath method (ArcRoute)
295
+ if (typeof (element as any).matchesRoutePath === "function") {
296
+ const { matches, params } = (element as any).matchesRoutePath(
297
+ url.pathname,
298
+ );
299
+ if (matches) {
300
+ matchedRoute = element;
301
+ routeParams = params || {};
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ if (!matchedRoute) {
308
+ return new Response("Route not found", { status: 404 });
309
+ }
310
+
311
+ const handler = matchedRoute.getHandler(method);
312
+ if (!handler) {
313
+ return new Response(`Method ${method} not allowed`, { status: 405 });
314
+ }
315
+
316
+ try {
317
+ // Extract token from Authorization header
318
+ const authHeader = req.headers.get("Authorization");
319
+ const token = authHeader?.replace("Bearer ", "");
320
+
321
+ const clientIp = this.getClientIpAddress(req);
322
+ let authContext: AuthContext;
323
+
324
+ if (token && !matchedRoute.isPublic) {
325
+ const payload = await this.verifyAuthToken(token);
326
+ if (!payload) {
327
+ return new Response("Invalid or expired token", { status: 401 });
328
+ }
329
+ authContext = this.tokenToAuthContext(payload, clientIp);
330
+ } else {
331
+ authContext = this.getDefaultAuthContext(clientIp);
332
+ }
333
+
334
+ // Use the model's routes method to properly handle event publishing
335
+ const routes = this.model.routes(authContext);
336
+ const response = await routes[matchedRoute.name](
337
+ method,
338
+ req,
339
+ routeParams,
340
+ url,
341
+ );
342
+ return response;
343
+ } catch (error) {
344
+ console.error(`Error executing route ${matchedRoute.name}:`, error);
345
+ return new Response("Internal Server Error", { status: 500 });
346
+ }
347
+ }
348
+
285
349
  private setupServer() {
286
350
  this.server = Bun.serve({
287
351
  fetch: async (req, server) => {
@@ -328,6 +392,12 @@ class RTCHost implements RealTimeCommunicationAdapter {
328
392
  return await this.handleQuery(req);
329
393
  }
330
394
 
395
+ // Try to handle as a route
396
+ const routeResponse = await this.handleRoute(req);
397
+ if (routeResponse.status !== 404) {
398
+ return routeResponse;
399
+ }
400
+
331
401
  return new Response("Not Found", { status: 404 });
332
402
  },
333
403
  websocket: {
@@ -344,25 +414,31 @@ class RTCHost implements RealTimeCommunicationAdapter {
344
414
  }
345
415
 
346
416
  private isPublicEndpoint(pathname: string): boolean {
347
- // Extract command name from pathname
348
- if (!pathname.startsWith("/command/")) {
349
- return false;
350
- }
417
+ // Iterate through all context elements and check if any match and are public
418
+ for (const element of this.context.elements) {
419
+ // Check if element has matchesCommandPath method (ArcCommand)
420
+ if (typeof (element as any).matchesCommandPath === "function") {
421
+ const { matches, isPublic } = (element as any).matchesCommandPath(
422
+ pathname,
423
+ );
424
+ if (matches) {
425
+ return isPublic;
426
+ }
427
+ }
351
428
 
352
- const commandName = pathname.split("/command/")[1];
353
- if (!commandName) {
354
- return false;
429
+ // Check if element has matchesRoutePath method (ArcRoute)
430
+ if (typeof (element as any).matchesRoutePath === "function") {
431
+ const { matches, isPublic } = (element as any).matchesRoutePath(
432
+ pathname,
433
+ );
434
+ if (matches) {
435
+ return isPublic;
436
+ }
437
+ }
355
438
  }
356
439
 
357
- // Get the command from the context and check if it's marked as public
358
- const contextElement = this.context.elements.find(
359
- (element: any) => element.name === commandName,
360
- );
361
-
362
- // Check if it's an ArcCommand instance and if it's marked as public
363
- return contextElement && "isPublic" in contextElement
364
- ? contextElement.isPublic === true
365
- : false;
440
+ // Default to non-public if no matching element found
441
+ return false;
366
442
  }
367
443
 
368
444
  private async handleSync(lastDate: string | null) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.1.5",
7
+ "version": "0.1.6",
8
8
  "private": false,
9
9
  "author": "Przemysław Krasiński [arcote.tech]",
10
10
  "dependencies": {