File size: 5,072 Bytes
b39afbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
 * Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
 * All rights reserved.
 */

import {
  Integration,
  NodeProcessEnv,
  type IAPISignature,
  type IIntegrationsConfig,
  type IntegrationsManager
} from 'omni-shared';
import { type ServerIntegrationsManager } from '../core/ServerIntegrationsManager';
import { type FastifyServerService } from '../services/FastifyServerService';

// TODO: [security] - Make sure to never pass error objects back to the client as they may leak credentials

// Template API integration plugin.
// usage:
//  - create a new class extending APIIntegration
//  - register the integration in the IntegrationsManager
//  - add desired routes and proxy entries to mercs configuration

interface IAPIIntegrationConfig extends IIntegrationsConfig {
  endpoints: string[];
  routes?: IAPISignature[];
}

class APIIntegration extends Integration {
  handlers: Map<string, Function>;
  clientExports: Map<string, Function>;
  serverHandlers: Map<string, Function>;
  routes: Set<IAPISignature>;
  schemas: Map<string, any>;

  constructor(id: string, manager: IntegrationsManager, config: IAPIIntegrationConfig) {
    super(id, manager, config || {});

    this.routes = new Set();

    this.handlers = new Map(); // TODO: <-- [georg] this needs to go away in favor of server handlers
    this.clientExports = new Map();
    this.serverHandlers = new Map();
    this.schemas = new Map();
  }

  declareClientExport(clientExport: any) {
    const manager = this.manager as ServerIntegrationsManager;
    if (!manager.clientExports.has(clientExport)) {
      manager.clientExports.add(clientExport);
    }
  }

  getEndpoint(route?: string): string {
    let ret = (this.config as IAPIIntegrationConfig).endpoints[0];
    if (route) {
      ret += route;
    }
    return ret;
  }

  addRoute(route: IAPISignature) {
    this.routes.add(route);
  }

  replaceTokens(string: string, field: string): any {
    const ret = string.replace(/\$\{([^}]+)\}/g, (match: any, p1: any) => {
      if (!Object.keys(this.config).includes(p1)) {
        // if the config value is not found, check if it's a function
        // @ts-ignore
        if (this[p1] != null && typeof this[p1] === 'function') {
          // @ts-ignore
          return this[p1]();
        } else {
          this.warn('replaceTokens: Unable to resolve variable', p1, 'in field ', field);
          return undefined;
        }
      } else {
        // @ts-ignore
        return this.config[p1];
      }
    });
    this.verbose('replaceTokens', field, ret);
    return ret;
  }

  async load() {
    const config = JSON.parse(JSON.stringify(this.config as IAPIIntegrationConfig));
    if (!this.app.services.has('httpd')) {
      this.warn('API service not found, cannot register routes');
      return false;
    }

    this.debug(`${this.id} integration loading...`);
    // Auto register any routes found in the config
    for (const path in config.routes || []) {
      // @ts-ignore
      const def = config.routes[path];
      if (def == null) {
        this.warn('Empty route definition: null', path);
        continue;
      }
      let method = 'GET';
      let endpoint = path;
      // routes can be denoted as 'GET /path' or just '/path'
      if (path.includes(' ')) {
        [method, endpoint] = path.split(' ');
      }

      // clone the object to avoid overwriting the original configuration
      const route = JSON.parse(JSON.stringify(def));
      route.method = method;

      if (this.handlers.has(route.handler)) {
        const apiDef: any = this.handlers.get(route.handler);
        // @ts-ignores
        const { handler, schema } = apiDef(this, def.opts);
        route.handler = handler;
        route.schema = schema;

        // If the route has a client export, we need to declare it as such
        if (this.clientExports.has(route.clientExport)) {
          // @ts-ignore
          const clientExport = this.clientExports.get(route.clientExport)();
          clientExport.namespace = this.id;
          clientExport.name = route.clientExport;
          clientExport.method = route.method;
          clientExport.endpoint = endpoint;
          this.declareClientExport(clientExport);
        }
      } else {
        this.error(
          endpoint,
          'route handler function not found, have you added it to the integrations handler Map?',
          route.handler
        );
        continue;
      }

      this.debug(`${this.id}: addRoute`, route.method, endpoint, 'handler installed');

      if (route.insecure && process.env.NODE_ENV === NodeProcessEnv.production) {
        this.warn(`${this.id}: route`, route.method, endpoint, 'is not secured by token.');
      }

      this.addRoute({ url: endpoint, ...route });
    }

    const api: FastifyServerService = this.app.services.get('httpd') as FastifyServerService;

    this.routes.forEach((route: IAPISignature) => {
      api.registerAPI(route);
    });

    this.success(`${this.id} integration loaded.`);
    return true;
  }
}

export { APIIntegration, type IAPIIntegrationConfig };