@usageflow/core 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/base.ts CHANGED
@@ -318,7 +318,7 @@ export abstract class UsageFlowAPI {
318
318
  public guessLedgerId(
319
319
  request: UsageFlowRequest,
320
320
  overrideUrl?: string,
321
- ): { ledgerId: string, hasLimit: boolean } {
321
+ ): { ledgerId: string, hasLimit: boolean, responseTrackingField?: string } {
322
322
  const method = request.method;
323
323
  const url = overrideUrl || this.getRoutePattern(request);
324
324
  const configs = this.apiConfigs;
@@ -331,23 +331,25 @@ export abstract class UsageFlowAPI {
331
331
  const fieldName = config.identityFieldName!;
332
332
  const location = config.identityFieldLocation;
333
333
  const hasLimit = config.hasRateLimit || false;
334
+ const isResponseTrackingEnabled = config.isResponseTrackingEnabled;
335
+ const responseTrackingField = isResponseTrackingEnabled ? config.responseTrackingField : undefined;
334
336
 
335
337
  if (method === config.method && url === config.url) {
336
338
 
337
339
  switch (location) {
338
340
  case "path_params":
339
341
  if (request.params?.[fieldName]) {
340
- return { ledgerId: `${method} ${url} ${request.params[fieldName]}`, hasLimit };
342
+ return { ledgerId: `${method} ${url} ${request.params[fieldName]}`, hasLimit, responseTrackingField };
341
343
  }
342
344
  break;
343
345
  case "query_params":
344
346
  if (request.query?.[fieldName]) {
345
- return { ledgerId: `${method} ${url} ${request.query[fieldName]}`, hasLimit };
347
+ return { ledgerId: `${method} ${url} ${request.query[fieldName]}`, hasLimit, responseTrackingField };
346
348
  }
347
349
  break;
348
350
  case "body":
349
351
  if (request.body?.[fieldName]) {
350
- return { ledgerId: `${method} ${url} ${request.body[fieldName]}`, hasLimit };
352
+ return { ledgerId: `${method} ${url} ${request.body[fieldName]}`, hasLimit, responseTrackingField };
351
353
  }
352
354
  break;
353
355
  case "bearer_token":
@@ -359,7 +361,7 @@ export abstract class UsageFlowAPI {
359
361
  if (token) {
360
362
  const claims = this.decodeJwtUnverified(token);
361
363
  if (claims?.[fieldName]) {
362
- return { ledgerId: `${method} ${url} ${claims[fieldName]}`, hasLimit };
364
+ return { ledgerId: `${method} ${url} ${claims[fieldName]}`, hasLimit, responseTrackingField };
363
365
  }
364
366
  }
365
367
  break;
@@ -367,7 +369,7 @@ export abstract class UsageFlowAPI {
367
369
  case "headers": {
368
370
  const headerValue = this.getHeaderValue(request.headers, fieldName);
369
371
  if (headerValue) {
370
- return { ledgerId: `${method} ${url} ${headerValue}`, hasLimit };
372
+ return { ledgerId: `${method} ${url} ${headerValue}`, hasLimit, responseTrackingField };
371
373
  }
372
374
  break;
373
375
  }
@@ -381,7 +383,7 @@ export abstract class UsageFlowAPI {
381
383
  if (cookieValue) {
382
384
  const claims = this.decodeJwtUnverified(cookieValue);
383
385
  if (claims?.[claim]) {
384
- return { ledgerId: `${method} ${url} ${claims[claim]}`, hasLimit };
386
+ return { ledgerId: `${method} ${url} ${claims[claim]}`, hasLimit, responseTrackingField };
385
387
  }
386
388
  }
387
389
  break;
@@ -398,7 +400,7 @@ export abstract class UsageFlowAPI {
398
400
  }
399
401
 
400
402
  if (cookieValue) {
401
- return { ledgerId: `${method} ${url} ${cookieValue}`, hasLimit };
403
+ return { ledgerId: `${method} ${url} ${cookieValue}`, hasLimit, responseTrackingField };
402
404
  }
403
405
  break;
404
406
  }
@@ -406,7 +408,7 @@ export abstract class UsageFlowAPI {
406
408
  }
407
409
  }
408
410
 
409
- return { ledgerId: `${method} ${url}`, hasLimit: false };
411
+ return { ledgerId: `${method} ${url}`, hasLimit: false, responseTrackingField: undefined };
410
412
  }
411
413
 
412
414
  private async fetchApiPolicies(): Promise<void> {
@@ -476,6 +478,7 @@ export abstract class UsageFlowAPI {
476
478
  payload: RequestForAllocation,
477
479
  metadata: RequestMetadata,
478
480
  hasLimit: boolean,
481
+ responseTrackingField?: string,
479
482
  ): Promise<void> {
480
483
  if (this.socketManager && this.socketManager.isConnected()) {
481
484
  try {
@@ -508,6 +511,7 @@ export abstract class UsageFlowAPI {
508
511
  });
509
512
  request.usageflow!.eventId = payload.allocationId;
510
513
  request.usageflow!.metadata = metadata;
514
+ request.usageflow!.responseTrackingField = responseTrackingField;
511
515
  }
512
516
  } catch (error: any) {
513
517
  console.error(
@@ -684,6 +688,109 @@ export abstract class UsageFlowAPI {
684
688
  return path.split('.').reduce((acc, part) => acc?.[part], object);
685
689
  }
686
690
 
691
+ /**
692
+ * Extract a value from an object using a dot-notation path
693
+ * Supports nested paths like "data.amount" or "result.items[0].count"
694
+ * Supports wildcard array notation like "items[*].id" to iterate over arrays
695
+ * @param obj - The object to extract the value from
696
+ * @param path - The dot-notation path to the value (e.g., "data.amount" or "items[*].id")
697
+ * @returns The value at the path, or undefined if not found. For wildcard arrays, returns an array of values.
698
+ */
699
+ public getValueByPath(obj: any, path: string): any {
700
+ if (!obj || !path) {
701
+ return undefined;
702
+ }
703
+
704
+ // Parse path into segments, preserving [*] as a special marker
705
+ const segments: Array<string | '*'> = [];
706
+ let currentSegment = '';
707
+ let i = 0;
708
+
709
+ while (i < path.length) {
710
+ if (path[i] === '[' && i + 1 < path.length && path[i + 1] === '*') {
711
+ // Found [*]
712
+ if (currentSegment) {
713
+ segments.push(currentSegment);
714
+ currentSegment = '';
715
+ }
716
+ segments.push('*');
717
+ i += 3; // Skip '[', '*', ']'
718
+ } else if (path[i] === '[') {
719
+ // Found indexed array access like [0]
720
+ if (currentSegment) {
721
+ segments.push(currentSegment);
722
+ currentSegment = '';
723
+ }
724
+ // Extract the index
725
+ let index = '';
726
+ i++; // Skip '['
727
+ while (i < path.length && path[i] !== ']') {
728
+ index += path[i];
729
+ i++;
730
+ }
731
+ if (index) {
732
+ segments.push(index);
733
+ }
734
+ i++; // Skip ']'
735
+ } else if (path[i] === '.') {
736
+ if (currentSegment) {
737
+ segments.push(currentSegment);
738
+ currentSegment = '';
739
+ }
740
+ i++;
741
+ } else {
742
+ currentSegment += path[i];
743
+ i++;
744
+ }
745
+ }
746
+
747
+ if (currentSegment) {
748
+ segments.push(currentSegment);
749
+ }
750
+
751
+ // Recursive helper function to process segments
752
+ const processSegments = (currentObj: any, segs: Array<string | '*'>, segIndex: number): any => {
753
+ if (currentObj === null || currentObj === undefined) {
754
+ return undefined;
755
+ }
756
+
757
+ // If we've processed all segments, return the current value
758
+ if (segIndex >= segs.length) {
759
+ return currentObj;
760
+ }
761
+
762
+ const segment = segs[segIndex];
763
+
764
+ // Handle wildcard array iteration
765
+ if (segment === '*') {
766
+ if (!Array.isArray(currentObj)) {
767
+ return undefined;
768
+ }
769
+
770
+ // If this is the last segment, return the first element of the array
771
+ if (segIndex === segs.length - 1) {
772
+ return currentObj.length > 0 ? currentObj[0] : undefined;
773
+ }
774
+
775
+ // Otherwise, iterate over the array and return the first valid result
776
+ for (const item of currentObj) {
777
+ const result = processSegments(item, segs, segIndex + 1);
778
+ if (result !== undefined) {
779
+ return result;
780
+ }
781
+ }
782
+
783
+ return undefined;
784
+ }
785
+
786
+ // Handle regular property access
787
+ const nextObj = currentObj[segment];
788
+ return processSegments(nextObj, segs, segIndex + 1);
789
+ };
790
+
791
+ return processSegments(obj, segments, 0);
792
+ }
793
+
687
794
  /**
688
795
  * Parse and extract a specific cookie value from the Cookie header
689
796
  * @param headers The request headers object
@@ -753,4 +860,110 @@ export abstract class UsageFlowAPI {
753
860
  claim: pickMatch[1],
754
861
  };
755
862
  }
863
+
864
+ public getResponseBody(response: Response): any {
865
+ if (response.body) {
866
+ return response.body;
867
+ }
868
+ return null;
869
+ }
870
+
871
+ /**
872
+ * Extract schema from a JavaScript object/array
873
+ * Recursively processes objects, arrays, and primitives to build a schema structure
874
+ * @param obj - The object to extract schema from
875
+ * @param path - The current path in the object hierarchy (for nested objects)
876
+ * @returns Schema object with type information and paths
877
+ */
878
+ public extractSchema(obj: any, path: string = ''): any {
879
+ // Handle null
880
+ if (obj === null || obj === undefined) {
881
+ return 'null';
882
+ }
883
+
884
+ // Handle arrays
885
+ if (Array.isArray(obj)) {
886
+ if (obj.length === 0) {
887
+ return 'array';
888
+ }
889
+ // Get schema of first element as representative
890
+ return {
891
+ type: 'array',
892
+ items: this.extractSchema(obj[0], path),
893
+ };
894
+ }
895
+
896
+ // Handle objects
897
+ if (typeof obj === 'object' && obj !== null) {
898
+ const schema: Record<string, any> = {};
899
+ for (const [key, value] of Object.entries(obj)) {
900
+ const fieldPath = path ? `${path}.${key}` : key;
901
+
902
+ if (value === null || value === undefined) {
903
+ schema[key] = {
904
+ type: 'null',
905
+ path: fieldPath,
906
+ };
907
+ } else if (typeof value === 'boolean') {
908
+ // Check bool before number (important for type detection)
909
+ schema[key] = {
910
+ type: 'boolean',
911
+ path: fieldPath,
912
+ };
913
+ } else if (typeof value === 'number') {
914
+ // Check if it's an integer or float
915
+ if (Number.isInteger(value)) {
916
+ schema[key] = {
917
+ type: 'integer',
918
+ path: fieldPath,
919
+ };
920
+ } else {
921
+ schema[key] = {
922
+ type: 'float',
923
+ path: fieldPath,
924
+ };
925
+ }
926
+ } else if (typeof value === 'string') {
927
+ schema[key] = {
928
+ type: 'string',
929
+ path: fieldPath,
930
+ };
931
+ } else if (Array.isArray(value)) {
932
+ schema[key] = {
933
+ type: 'array',
934
+ path: fieldPath,
935
+ items: value.length > 0 ? this.extractSchema(value[0], fieldPath) : 'unknown',
936
+ };
937
+ } else if (typeof value === 'object') {
938
+ schema[key] = {
939
+ type: 'object',
940
+ path: fieldPath,
941
+ properties: this.extractSchema(value, fieldPath),
942
+ };
943
+ } else {
944
+ schema[key] = {
945
+ type: typeof value,
946
+ path: fieldPath,
947
+ };
948
+ }
949
+ }
950
+
951
+ return schema;
952
+ }
953
+
954
+ // Handle primitive types
955
+ if (typeof obj === 'boolean') {
956
+ return 'boolean';
957
+ } else if (typeof obj === 'number') {
958
+ if (Number.isInteger(obj)) {
959
+ return 'integer';
960
+ } else {
961
+ return 'float';
962
+ }
963
+ } else if (typeof obj === 'string') {
964
+ return 'string';
965
+ } else {
966
+ return typeof obj;
967
+ }
968
+ }
756
969
  }
package/src/socket.ts CHANGED
@@ -11,6 +11,7 @@ interface PooledConnection {
11
11
  export class UsageFlowSocketManger {
12
12
  private connections: PooledConnection[] = [];
13
13
  private wsUrl: string = "wss://api.usageflow.io/ws";
14
+ // private wsUrl: string = "ws://localhost:9000/ws";
14
15
  private poolSize: number = 10; // Default pool size
15
16
  private currentIndex: number = 0; // For round-robin selection
16
17
  private connecting: boolean = false;
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ declare global {
5
5
  startTime: number;
6
6
  eventId?: string;
7
7
  metadata?: RequestMetadata;
8
+ responseTrackingField?: string;
8
9
  };
9
10
  baseUrl: string;
10
11
  }
@@ -16,6 +17,8 @@ declare global {
16
17
  startTime: number;
17
18
  eventId?: string;
18
19
  metadata?: RequestMetadata;
20
+ responseTrackingField?: string;
21
+
19
22
  };
20
23
  }
21
24
  }
@@ -26,6 +29,7 @@ declare global {
26
29
  startTime: number;
27
30
  eventId?: string;
28
31
  metadata?: RequestMetadata;
32
+ responseTrackingField?: string;
29
33
  };
30
34
  }
31
35
  }
@@ -63,6 +67,9 @@ export interface UsageFlowConfig {
63
67
  identityFieldName?: string;
64
68
  identityFieldLocation?: string;
65
69
  hasRateLimit?: boolean;
70
+ responseTrackingField?: string;
71
+ isResponseTrackingEnabled?: boolean;
72
+
66
73
  }
67
74
 
68
75
  export interface BlockedEndpoint {
@@ -140,6 +147,7 @@ export interface UsageFlowRequest {
140
147
  eventId?: string;
141
148
  metadata?: RequestMetadata;
142
149
  startTime?: number;
150
+ responseTrackingField?: string;
143
151
  };
144
152
  routeOptions?: {
145
153
  url: string;