@tymber/common 0.0.1-alpha.0 → 0.0.1

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.
Files changed (128) hide show
  1. package/LICENSE +22 -0
  2. package/dist/App.d.ts +14 -0
  3. package/dist/App.js +227 -0
  4. package/dist/Component.d.ts +16 -0
  5. package/dist/Component.js +116 -0
  6. package/dist/ConfigService.d.ts +31 -0
  7. package/dist/ConfigService.js +75 -0
  8. package/dist/Context.d.ts +32 -0
  9. package/dist/Context.js +10 -0
  10. package/dist/DB.d.ts +15 -0
  11. package/dist/DB.js +7 -0
  12. package/dist/Endpoint.d.ts +21 -0
  13. package/dist/Endpoint.js +87 -0
  14. package/dist/EventEmitter.d.ts +9 -0
  15. package/dist/EventEmitter.js +21 -0
  16. package/dist/Handler.d.ts +8 -0
  17. package/dist/Handler.js +8 -0
  18. package/dist/HttpContext.d.ts +24 -0
  19. package/dist/HttpContext.js +10 -0
  20. package/dist/I18nService.d.ts +18 -0
  21. package/dist/I18nService.js +72 -0
  22. package/dist/Middleware.d.ts +6 -0
  23. package/dist/Middleware.js +4 -0
  24. package/dist/Module.d.ts +47 -0
  25. package/dist/Module.js +12 -0
  26. package/dist/PubSubService.d.ts +20 -0
  27. package/dist/PubSubService.js +60 -0
  28. package/dist/Repository.d.ts +48 -0
  29. package/dist/Repository.js +133 -0
  30. package/dist/Router.d.ts +10 -0
  31. package/dist/Router.js +53 -0
  32. package/dist/TemplateService.d.ts +22 -0
  33. package/dist/TemplateService.js +66 -0
  34. package/dist/View.d.ts +17 -0
  35. package/dist/View.js +48 -0
  36. package/dist/ViewRenderer.d.ts +15 -0
  37. package/dist/ViewRenderer.js +58 -0
  38. package/dist/contrib/accept-language-parser.d.ts +9 -0
  39. package/dist/contrib/accept-language-parser.js +73 -0
  40. package/dist/contrib/cookie.d.ts +33 -0
  41. package/dist/contrib/cookie.js +207 -0
  42. package/dist/contrib/template.d.ts +1 -0
  43. package/dist/contrib/template.js +107 -0
  44. package/dist/index.d.ts +31 -0
  45. package/dist/index.js +31 -0
  46. package/dist/utils/ajv.d.ts +2 -0
  47. package/dist/utils/ajv.js +10 -0
  48. package/dist/utils/camelToSnakeCase.d.ts +1 -0
  49. package/dist/utils/camelToSnakeCase.js +3 -0
  50. package/dist/utils/computeBaseUrl.d.ts +1 -0
  51. package/dist/utils/computeBaseUrl.js +37 -0
  52. package/dist/utils/computeContentType.d.ts +1 -0
  53. package/dist/utils/computeContentType.js +17 -0
  54. package/dist/utils/createDebug.d.ts +1 -0
  55. package/dist/utils/createDebug.js +4 -0
  56. package/dist/utils/createTestApp.d.ts +8 -0
  57. package/dist/utils/createTestApp.js +54 -0
  58. package/dist/utils/escapeValue.d.ts +1 -0
  59. package/dist/utils/escapeValue.js +3 -0
  60. package/dist/utils/fs.d.ts +6 -0
  61. package/dist/utils/fs.js +13 -0
  62. package/dist/utils/isAdmin.d.ts +2 -0
  63. package/dist/utils/isAdmin.js +4 -0
  64. package/dist/utils/isProduction.d.ts +1 -0
  65. package/dist/utils/isProduction.js +1 -0
  66. package/dist/utils/loadModules.d.ts +3 -0
  67. package/dist/utils/loadModules.js +83 -0
  68. package/dist/utils/randomId.d.ts +1 -0
  69. package/dist/utils/randomId.js +4 -0
  70. package/dist/utils/randomUUID.d.ts +1 -0
  71. package/dist/utils/randomUUID.js +4 -0
  72. package/dist/utils/runMigrations.d.ts +3 -0
  73. package/dist/utils/runMigrations.js +85 -0
  74. package/dist/utils/snakeToCamelCase.d.ts +1 -0
  75. package/dist/utils/snakeToCamelCase.js +3 -0
  76. package/dist/utils/sortBy.d.ts +1 -0
  77. package/dist/utils/sortBy.js +8 -0
  78. package/dist/utils/sql.d.ts +120 -0
  79. package/dist/utils/sql.js +433 -0
  80. package/dist/utils/toNodeHandler.d.ts +2 -0
  81. package/dist/utils/toNodeHandler.js +91 -0
  82. package/dist/utils/types.d.ts +3 -0
  83. package/dist/utils/types.js +1 -0
  84. package/dist/utils/waitFor.d.ts +1 -0
  85. package/dist/utils/waitFor.js +5 -0
  86. package/package.json +28 -2
  87. package/src/App.ts +302 -0
  88. package/src/Component.ts +166 -0
  89. package/src/ConfigService.ts +121 -0
  90. package/src/Context.ts +49 -0
  91. package/src/DB.ts +28 -0
  92. package/src/Endpoint.ts +118 -0
  93. package/src/EventEmitter.ts +32 -0
  94. package/src/Handler.ts +14 -0
  95. package/src/HttpContext.ts +33 -0
  96. package/src/I18nService.ts +96 -0
  97. package/src/Middleware.ts +10 -0
  98. package/src/Module.ts +60 -0
  99. package/src/PubSubService.ts +77 -0
  100. package/src/Repository.ts +204 -0
  101. package/src/Router.ts +77 -0
  102. package/src/TemplateService.ts +97 -0
  103. package/src/View.ts +60 -0
  104. package/src/ViewRenderer.ts +71 -0
  105. package/src/contrib/accept-language-parser.ts +94 -0
  106. package/src/contrib/cookie.ts +256 -0
  107. package/src/contrib/template.ts +134 -0
  108. package/src/index.ts +57 -0
  109. package/src/utils/ajv.ts +13 -0
  110. package/src/utils/camelToSnakeCase.ts +3 -0
  111. package/src/utils/computeBaseUrl.ts +46 -0
  112. package/src/utils/computeContentType.ts +17 -0
  113. package/src/utils/createDebug.ts +5 -0
  114. package/src/utils/createTestApp.ts +81 -0
  115. package/src/utils/escapeValue.ts +3 -0
  116. package/src/utils/fs.ts +15 -0
  117. package/src/utils/isAdmin.ts +5 -0
  118. package/src/utils/isProduction.ts +2 -0
  119. package/src/utils/loadModules.ts +105 -0
  120. package/src/utils/randomId.ts +5 -0
  121. package/src/utils/randomUUID.ts +5 -0
  122. package/src/utils/runMigrations.ts +122 -0
  123. package/src/utils/snakeToCamelCase.ts +3 -0
  124. package/src/utils/sortBy.ts +8 -0
  125. package/src/utils/sql.ts +553 -0
  126. package/src/utils/toNodeHandler.ts +121 -0
  127. package/src/utils/types.ts +1 -0
  128. package/src/utils/waitFor.ts +5 -0
package/src/App.ts ADDED
@@ -0,0 +1,302 @@
1
+ import { type HttpMethod, Router } from "./Router.js";
2
+ import {
3
+ type Module,
4
+ type ModuleDefinition,
5
+ ModuleDefinitions,
6
+ } from "./Module.js";
7
+ import { Middleware } from "./Middleware.js";
8
+ import { parseCookieHeader } from "./contrib/cookie.js";
9
+ import { type HttpContext, HttpRedirectCode } from "./HttpContext.js";
10
+ import { computeBaseUrl } from "./utils/computeBaseUrl.js";
11
+ import { ViewRenderer } from "./ViewRenderer.js";
12
+ import { computeContentType } from "./utils/computeContentType.js";
13
+ import { loadModules } from "./utils/loadModules.js";
14
+ import { BaseI18nService } from "./I18nService.js";
15
+ import { BaseTemplateService } from "./TemplateService.js";
16
+ import { runMigrations } from "./utils/runMigrations.js";
17
+ import { createDebug } from "./utils/createDebug.js";
18
+ import type { DB } from "./DB.js";
19
+ import { isProduction } from "./utils/isProduction.js";
20
+ import { ComponentFactory } from "./Component.js";
21
+ import { PubSubService } from "./PubSubService.js";
22
+ import { FS } from "./utils/fs.js";
23
+ import { EnvironmentBasedConfigService } from "./ConfigService.js";
24
+ import { snakeToCamelCase } from "./utils/snakeToCamelCase.js";
25
+
26
+ const debug = createDebug("App");
27
+
28
+ function createRouter(modules: ModuleDefinition[]) {
29
+ const router = new Router();
30
+
31
+ for (const module of modules) {
32
+ for (const { method, path, handler } of module.endpoints) {
33
+ router.registerRoute(method, path, handler);
34
+ }
35
+
36
+ for (const { method, path, handler } of module.views) {
37
+ router.registerRoute(method, path, handler);
38
+ }
39
+
40
+ for (const { method, path, handler } of module.adminEndpoints) {
41
+ router.registerRoute(method, path, handler);
42
+ }
43
+
44
+ for (const { method, path, handler } of module.adminViews) {
45
+ router.registerRoute(method, path, handler);
46
+ }
47
+ }
48
+
49
+ return router;
50
+ }
51
+
52
+ function parseQueryParams(url: URL) {
53
+ const query = Object.create(null);
54
+
55
+ url.searchParams.forEach((value, key) => {
56
+ query[snakeToCamelCase(key)] = value;
57
+ });
58
+
59
+ return query;
60
+ }
61
+
62
+ export class App {
63
+ private readonly router: Router;
64
+ private readonly middlewares: Middleware[] = [];
65
+
66
+ private constructor(
67
+ modules: ModuleDefinition[],
68
+ private readonly assets: Map<string, string>,
69
+ private readonly viewRenderer: ViewRenderer,
70
+ ) {
71
+ this.router = createRouter(modules);
72
+ for (const module of modules) {
73
+ this.middlewares.push(...module.middlewares);
74
+ }
75
+ this.fetch = this.fetch.bind(this);
76
+ }
77
+
78
+ static async create(db: DB, modules: Module[]) {
79
+ debug(
80
+ "starting app in %s mode",
81
+ isProduction ? "production" : "development",
82
+ );
83
+
84
+ const componentFactory = new ComponentFactory();
85
+
86
+ let viewRenderer: ViewRenderer;
87
+
88
+ componentFactory.register(PubSubService);
89
+ componentFactory.register(EnvironmentBasedConfigService);
90
+ componentFactory.register(BaseI18nService);
91
+ componentFactory.register(BaseTemplateService);
92
+ componentFactory.register(ViewRenderer, (instance) => {
93
+ viewRenderer = instance;
94
+ });
95
+
96
+ debug("loading modules");
97
+ const moduleDefinitions = await loadModules(componentFactory, modules);
98
+ const components = componentFactory.build(
99
+ db,
100
+ new ModuleDefinitions(moduleDefinitions),
101
+ );
102
+
103
+ debug("running migrations");
104
+ await runMigrations(db, moduleDefinitions);
105
+
106
+ debug("initializing all components");
107
+ await Promise.all(components.map((component) => component.init()));
108
+
109
+ const assets = new Map<string, string>();
110
+
111
+ for (const module of modules) {
112
+ if (!module.assetsDir) {
113
+ continue;
114
+ }
115
+
116
+ try {
117
+ for (const filename of await FS.readDirRecursively(
118
+ FS.join(module.assetsDir, "static"),
119
+ )) {
120
+ assets.set(
121
+ "/static/" + filename,
122
+ FS.join(module.assetsDir, "static", filename),
123
+ );
124
+ }
125
+ } catch (e) {}
126
+ }
127
+
128
+ const app = new App(moduleDefinitions, assets, viewRenderer!);
129
+
130
+ app.close = () => {
131
+ debug("closing all components");
132
+ return Promise.all(
133
+ components.map((component) => component.close()),
134
+ ).then();
135
+ };
136
+
137
+ return app;
138
+ }
139
+
140
+ public async fetch(req: Request): Promise<Response> {
141
+ const url = new URL(req.url, "http://localhost");
142
+
143
+ const ctx = {
144
+ startedAt: new Date(),
145
+ method: req.method as HttpMethod,
146
+ path: url.pathname,
147
+ query: parseQueryParams(url),
148
+ headers: req.headers,
149
+ cookies: parseCookieHeader(req.headers.get("cookie") || ""),
150
+ abortSignal: req.signal,
151
+ responseHeaders: new Headers(),
152
+
153
+ render: async (view, data = {}) => {
154
+ try {
155
+ return this.viewRenderer.render(ctx, view, data);
156
+ } catch (e) {
157
+ return this.renderHttp500Error(ctx, e as Error);
158
+ }
159
+ },
160
+
161
+ redirect(
162
+ path: string,
163
+ code: HttpRedirectCode = HttpRedirectCode.HTTP_302_FOUND,
164
+ ) {
165
+ const baseUrl = computeBaseUrl(ctx.headers);
166
+ // note: `Response.redirect()` does not allow to specify additional headers
167
+ return new Response(null, {
168
+ status: code,
169
+ headers: {
170
+ Location: `${baseUrl}${path}`,
171
+ },
172
+ });
173
+ },
174
+ } as HttpContext;
175
+
176
+ if (ctx.path.startsWith("/static")) {
177
+ if (!this.assets.has(ctx.path)) {
178
+ return this.renderHttp404Error(ctx);
179
+ }
180
+ const contentType = computeContentType(ctx.path);
181
+ const absolutePath = this.assets.get(ctx.path)!;
182
+
183
+ debug("serving %s with content type %s", ctx.path, contentType);
184
+ return new Response(FS.createReadStream(absolutePath), {
185
+ headers: {
186
+ "content-type": contentType,
187
+ "cache-control": isProduction
188
+ ? "public, max-age=31536000, immutable"
189
+ : "no-cache",
190
+ },
191
+ });
192
+ }
193
+
194
+ // make a copy in case a middleware is removed
195
+ const middlewares = this.middlewares.slice();
196
+
197
+ for (const middleware of middlewares) {
198
+ try {
199
+ const httpRes = await middleware.handle(ctx);
200
+
201
+ if (httpRes) {
202
+ ctx.responseHeaders.forEach((value, key) => {
203
+ httpRes.headers.set(key, value);
204
+ });
205
+
206
+ return httpRes;
207
+ }
208
+ } catch (e) {
209
+ return this.renderHttp500Error(ctx, e as Error);
210
+ }
211
+ }
212
+
213
+ const route = this.router.findRoute(req.method as HttpMethod, url.pathname);
214
+
215
+ if (!route) {
216
+ return this.renderHttp404Error(ctx);
217
+ }
218
+
219
+ ctx.pathParams = route.pathParams;
220
+
221
+ const contentType = req.headers.get("content-type");
222
+
223
+ if (contentType) {
224
+ if (contentType !== "application/json") {
225
+ // TODO support other content types
226
+ return Response.json(
227
+ {
228
+ message: "unsupported media type",
229
+ },
230
+ {
231
+ status: 415,
232
+ },
233
+ );
234
+ }
235
+
236
+ try {
237
+ ctx.payload = await req.json();
238
+ } catch {
239
+ return Response.json(
240
+ {
241
+ message: "invalid request body",
242
+ },
243
+ {
244
+ status: 400,
245
+ },
246
+ );
247
+ }
248
+ }
249
+
250
+ try {
251
+ const httpRes = await route.handler.doHandle(ctx);
252
+
253
+ ctx.responseHeaders.forEach((value, key) => {
254
+ httpRes.headers.set(key, value);
255
+ });
256
+
257
+ return httpRes;
258
+ } catch (e) {
259
+ return this.renderHttp500Error(ctx, e as Error);
260
+ }
261
+ }
262
+
263
+ private renderHttp404Error(ctx: HttpContext) {
264
+ const acceptHeader = ctx.headers.get("accept");
265
+
266
+ if (acceptHeader?.includes("text/html")) {
267
+ return this.viewRenderer.render(ctx, "404");
268
+ } else {
269
+ return Response.json(
270
+ {
271
+ message: "resource not found",
272
+ },
273
+ {
274
+ status: 404,
275
+ },
276
+ );
277
+ }
278
+ }
279
+
280
+ private renderHttp500Error(ctx: HttpContext, error: Error) {
281
+ const acceptHeader = ctx.headers.get("accept");
282
+
283
+ if (acceptHeader?.includes("text/html")) {
284
+ return this.viewRenderer.render(ctx, "500", {
285
+ error,
286
+ });
287
+ } else {
288
+ return Response.json(
289
+ {
290
+ message: "an unexpected error occurred",
291
+ },
292
+ {
293
+ status: 500,
294
+ },
295
+ );
296
+ }
297
+ }
298
+
299
+ public close() {
300
+ return Promise.resolve();
301
+ }
302
+ }
@@ -0,0 +1,166 @@
1
+ import { type Context, type Span } from "./Context.js";
2
+
3
+ export const INJECT = Symbol("dependencies");
4
+
5
+ export type Ctor<T> = new (...args: any[]) => T;
6
+ type AbstractCtor<T> = abstract new (...args: any[]) => T;
7
+
8
+ type Dependency = Ctor<Component> | AbstractCtor<Component>;
9
+
10
+ export abstract class Component {
11
+ private __isComponent!: void; // nominal typing
12
+
13
+ static [INJECT]: Dependency[] = [];
14
+
15
+ init(): void | Promise<void> {}
16
+ close(): void | Promise<void> {}
17
+ }
18
+
19
+ interface Node {
20
+ ctor: Ctor<Component>;
21
+ dependencies: Ctor<Component>[];
22
+ level: number;
23
+ }
24
+
25
+ function isSubclass(subclass: any, superclass: any): boolean {
26
+ return superclass.prototype.isPrototypeOf(subclass.prototype);
27
+ }
28
+
29
+ function customConstructor<T extends { new (...args: any[]): any }>(
30
+ ctor: T,
31
+ onCreation: (instance: any) => void,
32
+ ): T {
33
+ return class extends ctor {
34
+ constructor(...args: any[]) {
35
+ super(...args);
36
+ onCreation(this);
37
+ }
38
+ };
39
+ }
40
+
41
+ export class ComponentFactory {
42
+ private readonly componentTree: Node[] = [];
43
+
44
+ public register<T extends Component>(
45
+ ctor: Ctor<T>,
46
+ onCreation?: (instance: T) => void,
47
+ ) {
48
+ if (onCreation) {
49
+ ctor = customConstructor(ctor, onCreation);
50
+ }
51
+
52
+ let level = 0;
53
+
54
+ // find place in the tree
55
+ for (const node of this.componentTree) {
56
+ for (const dependency of node.dependencies) {
57
+ if (ctor === dependency || isSubclass(ctor, dependency)) {
58
+ level = Math.max(level, node.level + 1);
59
+ }
60
+ }
61
+ }
62
+
63
+ // move dependencies at the bottom
64
+ // @ts-expect-error
65
+ const dependencies = (ctor[INJECT] || []) as Ctor<Component>[];
66
+ for (const node of this.componentTree) {
67
+ for (const dependency of dependencies) {
68
+ if (node.ctor === dependency || isSubclass(node.ctor, dependency)) {
69
+ node.level = Math.max(node.level, level + 1);
70
+ }
71
+ }
72
+ }
73
+
74
+ this.componentTree.push({
75
+ ctor,
76
+ dependencies,
77
+ level,
78
+ });
79
+ }
80
+
81
+ public build(...baseComponents: Component[]) {
82
+ const components: Component[] = [...baseComponents];
83
+
84
+ const proxyHandler: ProxyHandler<any> = {
85
+ get(target, prop) {
86
+ const method = target[prop];
87
+
88
+ if (typeof method !== "function") {
89
+ return method;
90
+ }
91
+
92
+ return function (...args: any[]) {
93
+ const isTracingEnabled = args.length > 0 && args[0].tracing?.enabled;
94
+ if (!isTracingEnabled) {
95
+ return method.apply(target, args);
96
+ }
97
+
98
+ const ctx = args[0] as Context;
99
+
100
+ const span: Span = {
101
+ component: target.constructor.name,
102
+ method: prop as string,
103
+ startedAt: Date.now(),
104
+ duration: -1,
105
+ isSuccess: false,
106
+ };
107
+
108
+ ctx.tracing?.spans.push(span);
109
+
110
+ function completeSpan() {
111
+ span.duration = Date.now() - span.startedAt;
112
+ }
113
+
114
+ try {
115
+ const result = method.apply(target, args);
116
+
117
+ if (isPromise(result)) {
118
+ return result
119
+ .then((output: any) => {
120
+ span.isSuccess = true;
121
+ return output;
122
+ })
123
+ .finally(() => completeSpan());
124
+ }
125
+
126
+ span.isSuccess = true;
127
+ return result;
128
+ } finally {
129
+ completeSpan();
130
+ }
131
+ };
132
+ },
133
+ };
134
+
135
+ this.componentTree.sort((a, b) => b.level - a.level);
136
+
137
+ // TODO do not instantiate components that are not used in the tree
138
+ for (const node of this.componentTree) {
139
+ const deps = node.dependencies.map((ctor) => {
140
+ const availableDeps = components.filter(
141
+ (component) => component instanceof ctor,
142
+ );
143
+
144
+ if (availableDeps.length === 0) {
145
+ throw `unresolved dependency ${ctor.name} for ${node.ctor.name}`;
146
+ }
147
+
148
+ return availableDeps[availableDeps.length - 1];
149
+ });
150
+
151
+ const component = new node.ctor(...deps);
152
+ components.push(new Proxy(component, proxyHandler));
153
+ }
154
+
155
+ return components;
156
+ }
157
+ }
158
+
159
+ function isPromise(obj: any) {
160
+ return (
161
+ !!obj &&
162
+ typeof obj === "object" &&
163
+ typeof obj.then === "function" &&
164
+ typeof obj.catch === "function"
165
+ );
166
+ }
@@ -0,0 +1,121 @@
1
+ import { hash } from "node:crypto";
2
+ import { Component } from "./Component.js";
3
+ import { type Context, emptyContext } from "./Context.js";
4
+
5
+ type ConfigType = "string" | "string[]" | "number" | "boolean";
6
+
7
+ interface ConfigDefinition {
8
+ key: string;
9
+ type: ConfigType;
10
+ defaultValue?: any;
11
+ shouldObfuscate?: boolean;
12
+ }
13
+
14
+ interface ConfigDefinitionWithValue extends ConfigDefinition {
15
+ value: any;
16
+ }
17
+
18
+ type ConfigValues = Record<string, any>;
19
+
20
+ interface ConfigHandler {
21
+ configDefinitions: ConfigDefinition[];
22
+ handler: (config: ConfigValues) => void | Promise<void>;
23
+ previousConfigHash: string;
24
+ }
25
+
26
+ export abstract class ConfigService extends Component {
27
+ protected handlers: ConfigHandler[] = [];
28
+
29
+ public subscribe(
30
+ configDefinitions: ConfigDefinition[],
31
+ handler: (config: ConfigValues) => void | Promise<void>,
32
+ ) {
33
+ this.handlers.push({
34
+ configDefinitions,
35
+ handler,
36
+ previousConfigHash: "",
37
+ });
38
+ }
39
+
40
+ override init() {
41
+ return this.notifyConsumers(emptyContext());
42
+ }
43
+
44
+ protected async notifyConsumers(ctx: Context) {
45
+ const values = await this.getCurrentValues(ctx);
46
+ for (const elem of this.handlers) {
47
+ const config: ConfigValues = {};
48
+
49
+ for (const { key, defaultValue } of elem.configDefinitions) {
50
+ config[key] = values[key] || defaultValue;
51
+ }
52
+
53
+ const configHash = hash("sha256", JSON.stringify(config));
54
+
55
+ if (configHash !== elem.previousConfigHash) {
56
+ elem.previousConfigHash = configHash;
57
+ await elem.handler(config);
58
+ }
59
+ }
60
+ }
61
+
62
+ public getDefinitions() {
63
+ return this.handlers.flatMap((elem) => elem.configDefinitions);
64
+ }
65
+
66
+ public async getCurrentConfig(ctx: Context) {
67
+ const values = await this.getCurrentValues(ctx);
68
+ const config: ConfigDefinitionWithValue[] = [];
69
+
70
+ for (const handler of this.handlers) {
71
+ for (const {
72
+ key,
73
+ type,
74
+ defaultValue,
75
+ shouldObfuscate,
76
+ } of handler.configDefinitions) {
77
+ config.push({
78
+ key,
79
+ type,
80
+ defaultValue,
81
+ value: values[key] ?? defaultValue,
82
+ shouldObfuscate,
83
+ });
84
+ }
85
+ }
86
+
87
+ return config;
88
+ }
89
+
90
+ protected abstract getCurrentValues(ctx: Context): Promise<ConfigValues>;
91
+ }
92
+
93
+ function formatValue(type: ConfigType, value: string) {
94
+ switch (type) {
95
+ case "string":
96
+ return value;
97
+ case "string[]":
98
+ return value.split(",");
99
+ case "number":
100
+ return parseInt(value, 10);
101
+ case "boolean":
102
+ return value === "1";
103
+ }
104
+ }
105
+
106
+ export class EnvironmentBasedConfigService extends ConfigService {
107
+ override getCurrentValues() {
108
+ const availableKeys = Object.keys(process.env);
109
+ const values: Record<string, any> = {};
110
+
111
+ for (const handler of this.handlers) {
112
+ for (const { key, type } of handler.configDefinitions) {
113
+ if (availableKeys.includes(key)) {
114
+ values[key] = formatValue(type, process.env[key] ?? "");
115
+ }
116
+ }
117
+ }
118
+
119
+ return Promise.resolve(values);
120
+ }
121
+ }
package/src/Context.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { type Brand } from "./utils/types.js";
2
+
3
+ export type UserId = Brand<string, "UserId">;
4
+ export type OrgId = Brand<string, "OrgId">;
5
+ export type AdminUserId = Brand<number, "AdminUserId">;
6
+
7
+ export interface User {
8
+ id: UserId;
9
+ orgs: Array<{
10
+ id: OrgId;
11
+ role: string;
12
+ }>;
13
+ }
14
+
15
+ export interface Admin {
16
+ id: AdminUserId;
17
+ }
18
+
19
+ export interface Span {
20
+ component: string;
21
+ method: string;
22
+ startedAt: number;
23
+ duration: number;
24
+ isSuccess: boolean;
25
+ }
26
+
27
+ export interface Context {
28
+ startedAt: Date;
29
+
30
+ tx?: any;
31
+
32
+ user?: User;
33
+ admin?: Admin;
34
+
35
+ tracing: {
36
+ enabled: boolean;
37
+ spans: Array<Span>;
38
+ };
39
+ }
40
+
41
+ export function emptyContext(): Context {
42
+ return {
43
+ startedAt: new Date(),
44
+ tracing: {
45
+ enabled: false,
46
+ spans: [],
47
+ },
48
+ };
49
+ }
package/src/DB.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { Component } from "./Component.js";
2
+ import { type Context } from "./Context.js";
3
+ import { Statement } from "./utils/sql.js";
4
+
5
+ export abstract class DB extends Component {
6
+ public abstract readonly name: string;
7
+
8
+ abstract query<T extends Record<string, any>>(
9
+ ctx: Context,
10
+ query: Statement,
11
+ ): Promise<T[]>;
12
+
13
+ abstract run(
14
+ ctx: Context,
15
+ query: Statement,
16
+ ): Promise<{ affectedRows: number }>;
17
+
18
+ abstract exec(ctx: Context, query: Statement): Promise<void>;
19
+
20
+ abstract startTransaction(
21
+ ctx: Context,
22
+ fn: () => Promise<void>,
23
+ ): void | Promise<void>;
24
+
25
+ abstract createMigrationsTable(ctx: Context): Promise<void>;
26
+ }
27
+
28
+ export class DuplicateKeyError extends Error {}