forked from restspace/rs-runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Modules.ts
297 lines (270 loc) · 13.8 KB
/
Modules.ts
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import { Service } from "rs-core/Service.ts";
import { IAdapter } from "rs-core/adapter/IAdapter.ts";
import Ajv, { ValidateFunction } from "https://cdn.skypack.dev/ajv?dts";
import { getErrors } from "rs-core/utility/errors.ts";
import { assignProperties } from "rs-core/utility/schema.ts";
import { IServiceConfig, IServiceConfigTemplate, schemaIServiceConfig } from "rs-core/IServiceConfig.ts";
import { Url } from "rs-core/Url.ts";
import { IAdapterManifest, IManifest, IServiceManifest } from "rs-core/IManifest.ts";
import { config, Infra } from "./config.ts";
import { IFileAdapter } from "rs-core/adapter/IFileAdapter.ts";
import LocalFileAdapter from "./adapter/LocalFileAdapter.ts";
import LocalFileAdapterManifest from "./adapter/LocalFileAdapter.ram.js";
import S3FileAdapter from "./adapter/S3FileAdapter.ts";
import S3FileAdapterManifest from "./adapter/S3FileAdapter.ram.js";
import NunjucksTemplateAdapter from "./adapter/NunjucksTemplateAdapter.ts";
import NunjucksTemplateAdapterManifest from "./adapter/NunjucksTemplateAdapter.ram.js";
import SimpleProxyAdapter from "./adapter/SimpleProxyAdapter.ts";
import SimpleProxyAdapterManifest from "./adapter/SimpleProxyAdapter.ram.js";
import AWS4ProxyAdapter from "./adapter/AWS4ProxyAdapter.ts";
import AWS4ProxyAdapterManifest from "./adapter/AWS4ProxyAdapter.ram.js";
import Services from "./services/services.ts";
import ServicesManifest from "./services/services.rsm.js";
import Auth from "./services/auth.ts";
import AuthManifest from "./services/auth.rsm.js";
import Data from "./services/data.ts";
import DataManifest from "./services/data.rsm.js";
import Dataset from "./services/dataset.ts";
import DatasetManifest from "./services/dataset.rsm.js";
import File from "./services/file.ts";
import FileManifest from "./services/file.rsm.js";
import Lib from "./services/lib.ts";
import LibManifest from "./services/lib.rsm.js";
import Pipeline from "./services/pipeline.ts";
import PipelineManifest from "./services/pipeline.rsm.js";
import StaticSiteFilter from "./services/static-site-filter.ts";
import StaticSiteFilterManifest from "./services/static-site-filter.rsm.js";
import StaticSiteManifest from "./services/static-site.rsm.js";
import UserDataManifest from "./services/user-data.rsm.js";
import UserFilterManifest from "./services/user-filter.rsm.js";
import UserFilter from "./services/user-filter.ts";
import Template from "./services/template.ts";
import TemplateManifest from "./services/template.rsm.js";
import Proxy from "./services/proxy.ts";
import ProxyManifest from "./services/proxy.rsm.js";
import Email from "./services/email.ts";
import EmailManifest from "./services/email.rsm.js";
import Account from "./services/account.ts";
import AccountManifest from "./services/account.rsm.js";
import Discord from "./services/discord.ts";
import DiscordManifest from "./services/discord.rsm.js";
import { AdapterContext } from "../rs-core/ServiceContext.ts";
import { makeServiceContext } from "./makeServiceContext.ts";
import { transformation } from "rs-core/transformation/transformation.ts";
export const schemaIServiceManifest = {
type: "object",
properties: {
"name": { type: "string" },
"description": { type: "string" },
"moduleUrl": { type: "string" },
"configSchema": {
type: "object",
properties: { }
},
"apis": {
type: "array",
items: { type: "string" }
},
"adapterInterface": { type: "string" },
"privateServices": { type: "object" },
"prePipeline": { type: "array" },
"postPipeline": { type: "array" }
},
required: [ "name", "description", "moduleUrl" ]
};
export const schemaIAdapterManifest = {
type: "object",
properties: {
"name": { type: "string" },
"description": { type: "string" },
"moduleUrl": { type: "string" },
"configSchema": {
type: "object",
properties: { }
},
"adapterInterfaces": {
type: "array",
items: { type: "string" }
}
},
required: [ "name", "description", "moduleUrl", "adapterInterfaces" ]
};
export function manifestIsService(manifest: IManifest): manifest is IServiceManifest {
return !("adapterInterfaces" in manifest);
}
export function applyServiceConfigTemplate(serviceConfig: IServiceConfig, configTemplate: IServiceConfigTemplate): IServiceConfig {
const transformObject = { ...configTemplate };
delete (transformObject as any).source;
const outputConfig = transformation(transformObject, serviceConfig, new Url(configTemplate.source));
outputConfig.source = configTemplate.source;
return outputConfig;
}
/** Modules is a singleton which holds compiled services and adapters for all tenants */
export class Modules {
adapterConstructors: { [ name: string ]: new (context: AdapterContext, config: unknown) => IAdapter } = {};
serviceManifests: { [ name: string ]: IServiceManifest } = {};
adapterManifests: { [ name: string ]: IAdapterManifest } = {};
services: { [ name: string ]: Service } = {};
validateServiceManifest: ValidateFunction<IServiceManifest>;
validateAdapterManifest: ValidateFunction<IAdapterManifest>;
validateAdapterConfig: { [ source: string ]: ValidateFunction } = {};
validateServiceConfig: { [ source: string ]: ValidateFunction } = {};
constructor(public ajv: Ajv) {
this.validateServiceManifest = ajv.compile<IServiceManifest>(schemaIServiceManifest);
this.validateAdapterManifest = ajv.compile<IAdapterManifest>(schemaIAdapterManifest);
// Statically load core services & adapters
this.adapterConstructors = {
"./adapter/LocalFileAdapter.ts": LocalFileAdapter,
"./adapter/S3FileAdapter.ts": S3FileAdapter,
"./adapter/NunjucksTemplateAdapter.ts": NunjucksTemplateAdapter,
"./adapter/SimpleProxyAdapter.ts": SimpleProxyAdapter as new (context: AdapterContext, props: unknown) => IAdapter,
"./adapter/AWS4ProxyAdapter.ts": AWS4ProxyAdapter as new (context: AdapterContext, props: unknown) => IAdapter
};
this.adapterManifests = {
"./adapter/LocalFileAdapter.ram.json": LocalFileAdapterManifest,
"./adapter/S3FileAdapter.ram.json": S3FileAdapterManifest,
"./adapter/NunjucksTemplateAdapter.ram.json": NunjucksTemplateAdapterManifest,
"./adapter/SimpleProxyAdapter.ram.json": SimpleProxyAdapterManifest,
"./adapter/AWS4ProxyAdapter.ram.json": AWS4ProxyAdapterManifest
};
Object.entries(this.adapterManifests).forEach(([url, v]) => {
(v as any).source = url;
this.validateAdapterConfig[url] = this.ajv.compile(this.adapterManifests[url].configSchema || {});
});
this.services = {
"./services/services.ts": Services,
"./services/auth.ts": Auth as unknown as Service<IAdapter, IServiceConfig>,
"./services/data.ts": Data as unknown as Service<IAdapter, IServiceConfig>,
"./services/dataset.ts": Dataset as unknown as Service<IAdapter, IServiceConfig>,
"./services/file.ts": File as unknown as Service<IAdapter, IServiceConfig>,
"./services/lib.ts": Lib,
"./services/pipeline.ts": Pipeline as unknown as Service<IAdapter, IServiceConfig>,
"./services/static-site-filter.ts": StaticSiteFilter as unknown as Service<IAdapter, IServiceConfig>,
"./services/user-filter.ts": UserFilter,
"./services/template.ts": Template as unknown as Service<IAdapter, IServiceConfig>,
"./services/proxy.ts": Proxy as unknown as Service<IAdapter, IServiceConfig>,
"./services/email.ts": Email as unknown as Service<IAdapter, IServiceConfig>,
"./services/account.ts": Account as unknown as Service<IAdapter, IServiceConfig>,
"./services/discord.ts": Discord as unknown as Service<IAdapter, IServiceConfig>
};
this.serviceManifests = {
"./services/services.rsm.json": ServicesManifest,
"./services/auth.rsm.json": AuthManifest,
"./services/data.rsm.json": DataManifest,
"./services/dataset.rsm.json": DatasetManifest,
"./services/file.rsm.json": FileManifest,
"./services/lib.rsm.json": LibManifest,
"./services/pipeline.rsm.json": PipelineManifest,
"./services/static-site-filter.rsm.json": StaticSiteFilterManifest,
"./services/static-site.rsm.json": StaticSiteManifest as unknown as IServiceManifest,
"./services/user-data.rsm.json": UserDataManifest as unknown as IServiceManifest,
"./services/user-filter.rsm.json": UserFilterManifest,
"./services/template.rsm.json": TemplateManifest as unknown as IServiceManifest,
"./services/proxy.rsm.json": ProxyManifest,
"./services/email.rsm.json": EmailManifest,
"./services/account.rsm.json": AccountManifest,
"./services/discord.rsm.json": DiscordManifest
};
Object.entries(this.serviceManifests).forEach(([url, v]) => {
(v as any).source = url;
this.ensureServiceConfigValidator(url);
});
}
async getConfigAdapter(tenant: string) {
const configStoreAdapterSpec = { ...config.server.infra[config.server.configStore] };
(configStoreAdapterSpec as Infra & { basePath: '/' }).basePath = "/";
const context = makeServiceContext(tenant);
const configAdapter = await config.modules.getAdapter<IFileAdapter>(configStoreAdapterSpec.adapterSource, context, configStoreAdapterSpec);
return configAdapter;
}
async getAdapterConstructor<T extends IAdapter>(url: string): Promise<new (context: AdapterContext, config: unknown) => T> {
if (!this.adapterConstructors[url]) {
try {
const module = await import(url);
this.adapterConstructors[url] = module.default;
} catch (err) {
throw new Error(`failed to load adapter at ${url}: ${err}`);
}
}
return this.adapterConstructors[url] as new (context: AdapterContext, config: unknown) => T;
}
async getAdapterManifest(url: string): Promise<IAdapterManifest | string> {
if (!this.adapterManifests[url]) {
try {
const manifestJson = await Deno.readTextFile(url);
const manifest = JSON.parse(manifestJson);
manifest.source = url;
this.adapterManifests[url] = manifest;
} catch (err) {
return `failed to load manifest at ${url}: ${err}`;
}
if (!this.validateAdapterManifest(this.adapterManifests[url])) {
return `bad format manifest at ${url}: ${getErrors(this.validateAdapterManifest)}`;
}
if (!this.validateAdapterConfig[url]) {
this.validateAdapterConfig[url] = this.ajv.compile(this.adapterManifests[url].configSchema || {})
}
}
return this.adapterManifests[url];
}
/** returns a new instance of an adapter */
async getAdapter<T extends IAdapter>(url: string, context: AdapterContext, config: unknown): Promise<T> {
if (url.split('?')[0].endsWith('.ram.json')) {
const manifest = await this.getAdapterManifest(url);
if (typeof manifest === 'string') throw new Error(manifest);
url = manifest.moduleUrl as string;
}
const constr = await this.getAdapterConstructor(url);
return new constr(context, config) as T;
}
ensureServiceConfigValidator(url: string) {
if (!this.validateServiceConfig[url]) {
let configSchema: Record<string, unknown> = schemaIServiceConfig;
const serviceManifest = this.serviceManifests[url];
if (serviceManifest.configSchema) {
configSchema = assignProperties(serviceManifest.configSchema, schemaIServiceConfig.properties, schemaIServiceConfig.required);
}
this.validateServiceConfig[url] = this.ajv.compile(configSchema);
}
}
async getServiceManifest(url: string): Promise<IServiceManifest | string> {
if (!this.serviceManifests[url]) {
try {
const manifestJson = await Deno.readTextFile(url);
const manifest = JSON.parse(manifestJson);
manifest.source = url;
this.serviceManifests[url] = manifest;
} catch (err) {
return `failed to load manifest at ${url}: ${err}`;
}
if (!this.validateServiceManifest(this.serviceManifests[url])) {
return `bad format manifest at ${url}: ${(this.validateServiceManifest.errors || []).map(e => e.message).join('; ')}`;
}
this.ensureServiceConfigValidator(url);
}
return this.serviceManifests[url];
}
async getService(url?: string): Promise<Service> {
if (url === undefined) {
return Service.Identity; // returns message unchanged
}
// pull manifest if necessary
if (url.split('?')[0].endsWith('.rsm.json')) {
const manifest = await this.getServiceManifest(url);
if (typeof manifest === 'string') throw new Error(manifest);
url = manifest.moduleUrl;
if (url === undefined) return Service.Identity;
}
if (!this.services[url]) {
try {
config.logger.debug(`Start -- loading service at ${url}`);
const module = await import(url);
this.services[url] = module.default;
config.logger.debug(`End -- loading service at ${url}`);
} catch (err) {
throw new Error(`failed to load module at ${url}: ${err}`);
}
}
return this.services[url];
}
}