@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
@@ -0,0 +1,134 @@
1
+ // from https://github.com/jashkenas/underscore/blob/master/modules/template.js
2
+
3
+ const escapeMap: Record<string, string> = {
4
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;",
7
+ '"': "&quot;",
8
+ };
9
+
10
+ const ESCAPE_REGEX = /[&<>"]/g;
11
+
12
+ const _ = {
13
+ escape(s: string) {
14
+ return s.replace(ESCAPE_REGEX, (match) => escapeMap[match]);
15
+ },
16
+ };
17
+
18
+ // When customizing `_.templateSettings`, if you don't want to define an
19
+ // interpolation, evaluation or escaping regex, we need one that is
20
+ // guaranteed not to match.
21
+ var noMatch = /(.)^/;
22
+
23
+ // Certain characters need to be escaped so that they can be put into a
24
+ // string literal.
25
+ var escapes: Record<string, string> = {
26
+ "'": "'",
27
+ "\\": "\\",
28
+ "\r": "r",
29
+ "\n": "n",
30
+ "\u2028": "u2028",
31
+ "\u2029": "u2029",
32
+ };
33
+
34
+ var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
35
+
36
+ function escapeChar(match: string) {
37
+ return "\\" + escapes[match];
38
+ }
39
+
40
+ // In order to prevent third-party code injection through
41
+ // `_.templateSettings.variable`, we test it against the following regular
42
+ // expression. It is intentionally a bit more liberal than just matching valid
43
+ // identifiers, but still prevents possible loopholes through defaults or
44
+ // destructuring assignment.
45
+ var bareIdentifier = /^\s*(\w|\$)+\s*$/;
46
+
47
+ // JavaScript micro-templating, similar to John Resig's implementation.
48
+ // Underscore templating handles arbitrary delimiters, preserves whitespace,
49
+ // and correctly escapes quotes within interpolated code.
50
+ // NB: `oldSettings` only exists for backwards compatibility.
51
+ export function compileTemplate(text: string): (data: any) => string {
52
+ const settings = {
53
+ evaluate: /<%([\s\S]+?)%>/g,
54
+ interpolate: /<%=([\s\S]+?)%>/g,
55
+ escape: /<%-([\s\S]+?)%>/g,
56
+ };
57
+
58
+ // Combine delimiters into one regular expression via alternation.
59
+ var matcher = RegExp(
60
+ [
61
+ (settings.escape || noMatch).source,
62
+ (settings.interpolate || noMatch).source,
63
+ (settings.evaluate || noMatch).source,
64
+ ].join("|") + "|$",
65
+ "g",
66
+ );
67
+
68
+ // Compile the template source, escaping string literals appropriately.
69
+ var index = 0;
70
+ var source = "__p+='";
71
+ text.replace(
72
+ matcher,
73
+ function (
74
+ match: string,
75
+ escape: string,
76
+ interpolate: string,
77
+ evaluate: string,
78
+ offset: number,
79
+ ) {
80
+ source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
81
+ index = offset + match.length;
82
+
83
+ if (escape) {
84
+ source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
85
+ } else if (interpolate) {
86
+ source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
87
+ } else if (evaluate) {
88
+ source += "';\n" + evaluate + "\n__p+='";
89
+ }
90
+
91
+ // Adobe VMs need the match returned to produce the correct offset.
92
+ return match;
93
+ },
94
+ );
95
+ source += "';\n";
96
+
97
+ // @ts-expect-error unknown property
98
+ var argument = settings.variable;
99
+ if (argument) {
100
+ // Insure against third-party code injection. (CVE-2021-23358)
101
+ if (!bareIdentifier.test(argument))
102
+ throw new Error("variable is not a bare identifier: " + argument);
103
+ } else {
104
+ // If a variable is not specified, place data values in local scope.
105
+ source = "with(obj||{}){\n" + source + "}\n";
106
+ argument = "obj";
107
+ }
108
+
109
+ source =
110
+ "var __t,__p='',__j=Array.prototype.join," +
111
+ "print=function(){__p+=__j.call(arguments,'');};\n" +
112
+ source +
113
+ "return __p;\n";
114
+
115
+ var render: Function;
116
+ try {
117
+ render = new Function(argument, "_", source);
118
+ } catch (e) {
119
+ // @ts-expect-error unknown property
120
+ (e as Error).source = source;
121
+ throw e;
122
+ }
123
+
124
+ var template = function (data: any) {
125
+ // @ts-expect-error unknown this
126
+ return render.call(this, data, _);
127
+ };
128
+
129
+ // Provide the compiled source as a convenience for precompilation.
130
+ // @ts-expect-error unknown property
131
+ template.source = "function(" + argument + "){\n" + source + "}";
132
+
133
+ return template;
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ export { Component, type Ctor, ComponentFactory, INJECT } from "./Component.js";
2
+ export {
3
+ type UserId,
4
+ type User,
5
+ type OrgId,
6
+ type AdminUserId,
7
+ type Admin,
8
+ type Context,
9
+ emptyContext,
10
+ } from "./Context.js";
11
+ export { ConfigService } from "./ConfigService.js";
12
+ export { DB, DuplicateKeyError } from "./DB.js";
13
+ export { EventEmitter } from "./EventEmitter.js";
14
+ export {
15
+ PubSubService,
16
+ NodeClusterPubSubService,
17
+ initPrimary,
18
+ } from "./PubSubService.js";
19
+ export { Endpoint, AdminEndpoint } from "./Endpoint.js";
20
+ export { type HttpContext } from "./HttpContext.js";
21
+ export { App } from "./App.js";
22
+ export {
23
+ type Module,
24
+ type AppInit,
25
+ ModuleDefinitions,
26
+ type Route,
27
+ } from "./Module.js";
28
+ export { View, AdminView } from "./View.js";
29
+ export { Middleware } from "./Middleware.js";
30
+ export {
31
+ Repository,
32
+ type Page,
33
+ type AuditedEntity,
34
+ AuditedRepository,
35
+ type AdminAuditedEntity,
36
+ AdminAuditedRepository,
37
+ } from "./Repository.js";
38
+ export { TemplateService } from "./TemplateService.js";
39
+
40
+ export { createCookie, parseCookieHeader } from "./contrib/cookie.js";
41
+
42
+ export { AJV_INSTANCE } from "./utils/ajv.js";
43
+ export { camelToSnakeCase } from "./utils/camelToSnakeCase.js";
44
+ export { createDebug } from "./utils/createDebug.js";
45
+ export { createTestApp, type BaseTestContext } from "./utils/createTestApp.js";
46
+ export { escapeValue } from "./utils/escapeValue.js";
47
+ export { FS } from "./utils/fs.js";
48
+ export { isAdmin } from "./utils/isAdmin.js";
49
+ export { isProduction } from "./utils/isProduction.js";
50
+ export { randomId } from "./utils/randomId.js";
51
+ export { randomUUID } from "./utils/randomUUID.js";
52
+ export { snakeToCamelCase } from "./utils/snakeToCamelCase.js";
53
+ export { sortBy } from "./utils/sortBy.js";
54
+ export { sql, Statement } from "./utils/sql.js";
55
+ export { toNodeHandler } from "./utils/toNodeHandler.js";
56
+ export { type Brand } from "./utils/types.js";
57
+ export { waitFor } from "./utils/waitFor.js";
@@ -0,0 +1,13 @@
1
+ import { Ajv } from "ajv";
2
+ import addFormats from "ajv-formats";
3
+
4
+ // reference: https://ajv.js.org/api.html
5
+
6
+ export const AJV_INSTANCE = new Ajv({
7
+ useDefaults: true,
8
+ coerceTypes: true,
9
+ removeAdditional: true,
10
+ });
11
+
12
+ // @ts-expect-error FIXME
13
+ addFormats(AJV_INSTANCE);
@@ -0,0 +1,3 @@
1
+ export function camelToSnakeCase(str: string) {
2
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
3
+ }
@@ -0,0 +1,46 @@
1
+ function removeQuotes(str: string) {
2
+ if (str.startsWith('"') && str.endsWith('"')) {
3
+ return str.slice(1, -1);
4
+ } else {
5
+ return str;
6
+ }
7
+ }
8
+
9
+ function parseForwardedHeader(header: string) {
10
+ const parts = header.split(";").map((part) => part.trim());
11
+ const parsed = {} as Record<string, string>;
12
+
13
+ for (const part of parts) {
14
+ const [key, value] = part.split("=");
15
+
16
+ if (key && value) {
17
+ parsed[key.toLowerCase()] = removeQuotes(value);
18
+ }
19
+ }
20
+
21
+ return `${parsed.proto}://${parsed.host}`;
22
+ }
23
+
24
+ export function computeBaseUrl(headers: Headers) {
25
+ const forwardedHeader = headers.get("forwarded");
26
+
27
+ if (forwardedHeader) {
28
+ const baseUrl = parseForwardedHeader(forwardedHeader);
29
+ if (baseUrl) {
30
+ return baseUrl;
31
+ }
32
+ }
33
+
34
+ const forwardedProto = headers.get("x-forwarded-proto");
35
+ const forwardedHost = headers.get("x-forwarded-host");
36
+
37
+ if (forwardedProto && forwardedHost) {
38
+ const proto = forwardedProto.split(",");
39
+ const host = forwardedHost.split(",");
40
+ return `${proto[0]}://${host[0]}`;
41
+ }
42
+
43
+ const host = headers.get("host");
44
+
45
+ return `http://${host}`;
46
+ }
@@ -0,0 +1,17 @@
1
+ const CONTENT_TYPES = new Map<string, string>([
2
+ ["html", "text/html"],
3
+ ["css", "text/css"],
4
+ ["js", "application/javascript"],
5
+ ["json", "application/json"],
6
+ ["png", "image/png"],
7
+ ["jpg", "image/jpeg"],
8
+ ["gif", "image/gif"],
9
+ ["svg", "image/svg+xml"],
10
+ ]);
11
+
12
+ export function computeContentType(path: string) {
13
+ const i = path.lastIndexOf(".");
14
+ if (i === -1) return "application/octet-stream";
15
+ const extension = path.substring(i + 1);
16
+ return CONTENT_TYPES.get(extension) || "application/octet-stream";
17
+ }
@@ -0,0 +1,5 @@
1
+ import { debuglog } from "node:util";
2
+
3
+ export function createDebug(module: string) {
4
+ return debuglog(`tymber:${module}`);
5
+ }
@@ -0,0 +1,81 @@
1
+ import { DB } from "../DB.js";
2
+ import { createServer, Server } from "node:http";
3
+ import { type AddressInfo } from "node:net";
4
+ import { type Module } from "../Module.js";
5
+ import { App } from "../App.js";
6
+ import { toNodeHandler } from "./toNodeHandler.js";
7
+
8
+ const CLOSE_DELAY_MS = 200;
9
+
10
+ export interface BaseTestContext {
11
+ baseUrl: string;
12
+ db: DB;
13
+ close: () => Promise<void>;
14
+ }
15
+
16
+ interface SharedTestContext {
17
+ httpServer: Server;
18
+ baseUrl: string;
19
+ db: DB;
20
+ }
21
+
22
+ let sharedTestContext: SharedTestContext | undefined;
23
+ let closeTimer: NodeJS.Timeout | undefined;
24
+
25
+ export async function createTestApp(
26
+ initDB: () => DB | Promise<DB>,
27
+ modules: Module[],
28
+ ): Promise<BaseTestContext> {
29
+ if (!sharedTestContext) {
30
+ const httpServer = createServer();
31
+
32
+ const [port, db] = await Promise.all([
33
+ startHttpServer(httpServer),
34
+ initDB(),
35
+ ]);
36
+ const baseUrl = `http://localhost:${port}`;
37
+
38
+ sharedTestContext = {
39
+ httpServer,
40
+ baseUrl,
41
+ db,
42
+ };
43
+ }
44
+
45
+ const { httpServer, baseUrl, db } = sharedTestContext;
46
+ const app = await App.create(db, modules);
47
+
48
+ httpServer.removeAllListeners("request");
49
+ httpServer.on("request", toNodeHandler(app.fetch.bind(app)));
50
+
51
+ return {
52
+ baseUrl,
53
+ db,
54
+ async close() {
55
+ clearTimeout(closeTimer);
56
+
57
+ closeTimer = setTimeout(async () => {
58
+ if (sharedTestContext) {
59
+ const { httpServer } = sharedTestContext;
60
+ sharedTestContext = undefined;
61
+
62
+ await Promise.all([closeHttpServer(httpServer), app.close()]);
63
+ }
64
+ }, CLOSE_DELAY_MS);
65
+ },
66
+ };
67
+ }
68
+
69
+ function startHttpServer(httpServer: Server) {
70
+ return new Promise<number>((resolve) => {
71
+ httpServer.listen(0, () => {
72
+ resolve((httpServer.address() as AddressInfo).port);
73
+ });
74
+ });
75
+ }
76
+
77
+ function closeHttpServer(httpServer: Server) {
78
+ return new Promise<void>((resolve) => {
79
+ httpServer.close(() => resolve());
80
+ });
81
+ }
@@ -0,0 +1,3 @@
1
+ export function escapeValue(value: string) {
2
+ return value.replaceAll(/([~%_])/g, "~$1");
3
+ }
@@ -0,0 +1,15 @@
1
+ import { join as nodeJoin } from "node:path";
2
+ import { readdir, readFile as nodeReadFile } from "node:fs/promises";
3
+ import { createReadStream as nodeCreateReadStream } from "node:fs";
4
+
5
+ // group Node.js-specific methods
6
+ export const FS = {
7
+ join: (...paths: string[]) => nodeJoin(...paths),
8
+ createReadStream: (path: string) => nodeCreateReadStream(path),
9
+ readFile: (path: string) => nodeReadFile(path, "utf8"),
10
+ readDirRecursively: (path: string) =>
11
+ readdir(path, {
12
+ encoding: "utf8",
13
+ recursive: true,
14
+ }),
15
+ };
@@ -0,0 +1,5 @@
1
+ import { type HttpContext } from "../HttpContext.js";
2
+
3
+ export function isAdmin(ctx: HttpContext) {
4
+ return ctx.admin !== undefined;
5
+ }
@@ -0,0 +1,2 @@
1
+ export const isProduction =
2
+ process.env.NODE_ENV === "production" || process.env.ENV === "production";
@@ -0,0 +1,105 @@
1
+ import { Component, ComponentFactory, type Ctor } from "../Component.js";
2
+ import type { Module, ModuleDefinition } from "../Module.js";
3
+ import { type HttpMethod } from "../Router.js";
4
+ import { AdminEndpoint, Endpoint } from "../Endpoint.js";
5
+ import { AdminView, View } from "../View.js";
6
+ import { Middleware } from "../Middleware.js";
7
+ import { createDebug } from "./createDebug.js";
8
+
9
+ const debug = createDebug("loadModules");
10
+
11
+ export async function loadModules(
12
+ componentFactory: ComponentFactory,
13
+ modules: Module[],
14
+ ) {
15
+ const moduleDefinitions = [];
16
+
17
+ for (const module of modules) {
18
+ const moduleDefinition: ModuleDefinition = {
19
+ name: module.name,
20
+ version: module.version,
21
+ assetsDir: module.assetsDir,
22
+ adminSidebarItems: module.adminSidebarItems,
23
+
24
+ endpoints: [],
25
+ views: [],
26
+ adminEndpoints: [],
27
+ adminViews: [],
28
+ middlewares: [],
29
+ };
30
+
31
+ const appInit = {
32
+ component<T extends Component>(ctor: Ctor<T>) {
33
+ debug("adding component %s", ctor.name);
34
+ componentFactory.register(ctor);
35
+ },
36
+
37
+ endpoint: (method: HttpMethod, path: string, ctor: Ctor<Endpoint>) => {
38
+ debug("adding endpoint %s %s", method, path);
39
+ componentFactory.register(ctor, (handler) => {
40
+ moduleDefinition.endpoints.push({
41
+ method,
42
+ path,
43
+ handlerName: ctor.name,
44
+ handler,
45
+ });
46
+ });
47
+ },
48
+
49
+ view: (path: string, ctor: Ctor<View>) => {
50
+ debug("adding view %s", path);
51
+ componentFactory.register(ctor, (handler) => {
52
+ moduleDefinition.views.push({
53
+ method: "GET",
54
+ path,
55
+ handlerName: ctor.name,
56
+ handler,
57
+ });
58
+ });
59
+ },
60
+
61
+ adminEndpoint: (
62
+ method: HttpMethod,
63
+ path: string,
64
+ ctor: Ctor<AdminEndpoint>,
65
+ ) => {
66
+ debug("adding admin endpoint %s %s", method, path);
67
+ componentFactory.register(ctor, (handler) => {
68
+ moduleDefinition.adminEndpoints.push({
69
+ method,
70
+ path,
71
+ handlerName: ctor.name,
72
+ handler,
73
+ });
74
+ });
75
+ },
76
+
77
+ adminView: (path: string, ctor: Ctor<AdminView>) => {
78
+ debug("adding admin view %s", path);
79
+ componentFactory.register(ctor, (handler) => {
80
+ moduleDefinition.adminViews.push({
81
+ method: "GET",
82
+ path,
83
+ handlerName: ctor.name,
84
+ handler,
85
+ });
86
+ });
87
+ },
88
+
89
+ middleware: (ctor: Ctor<Middleware>) => {
90
+ debug("adding middleware %s", ctor.name);
91
+
92
+ componentFactory.register(ctor, (instance) => {
93
+ moduleDefinition.middlewares.push(instance);
94
+ });
95
+ },
96
+ };
97
+
98
+ debug("loading module %s", module.name);
99
+ module.init(appInit);
100
+
101
+ moduleDefinitions.push(moduleDefinition);
102
+ }
103
+
104
+ return moduleDefinitions;
105
+ }
@@ -0,0 +1,5 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ export function randomId() {
4
+ return randomBytes(8).toString("hex");
5
+ }
@@ -0,0 +1,5 @@
1
+ import { randomUUID as nodeRandomUUID } from "node:crypto";
2
+
3
+ export function randomUUID() {
4
+ return nodeRandomUUID();
5
+ }
@@ -0,0 +1,122 @@
1
+ import type { ModuleDefinition } from "../Module.js";
2
+ import { sortBy } from "./sortBy.js";
3
+ import { createDebug } from "./createDebug.js";
4
+ import type { DB } from "../DB.js";
5
+ import { emptyContext } from "../Context.js";
6
+ import { FS } from "./fs.js";
7
+ import { sql } from "./sql.js";
8
+
9
+ const debug = createDebug("runMigrations");
10
+
11
+ // example: 0001-create-users-table.sql
12
+ const MIGRATION_REGEX = /^(\d+)-(.*)\.sql$/;
13
+
14
+ interface Migration {
15
+ module: string;
16
+ id: number;
17
+ name: string;
18
+ sql: string;
19
+ }
20
+
21
+ export async function runMigrations(db: DB, modules: ModuleDefinition[]) {
22
+ const ctx = emptyContext();
23
+
24
+ debug("creating migrations table");
25
+ await db.createMigrationsTable(ctx);
26
+
27
+ for (const module of modules) {
28
+ if (!module.assetsDir) {
29
+ continue;
30
+ }
31
+
32
+ const migrationFiles: Migration[] = [];
33
+
34
+ for (const { filename, absolutePath } of await readMigrationFiles(
35
+ db,
36
+ module,
37
+ )) {
38
+ let match = MIGRATION_REGEX.exec(filename);
39
+ if (match) {
40
+ migrationFiles.push({
41
+ module: module.name,
42
+ id: parseInt(match[1], 10),
43
+ name: match[2],
44
+ sql: await FS.readFile(absolutePath),
45
+ });
46
+ }
47
+ }
48
+
49
+ sortBy(migrationFiles, "id");
50
+
51
+ for (const migration of migrationFiles) {
52
+ await db.startTransaction(ctx, async () => {
53
+ const result = await db.query(
54
+ ctx,
55
+ sql.select().from("t_migrations").where({
56
+ id: migration.id,
57
+ module: migration.module,
58
+ }),
59
+ );
60
+
61
+ if (result.length === 1) {
62
+ debug(
63
+ "migration #%d of module %s already applied",
64
+ migration.id,
65
+ migration.module,
66
+ );
67
+ return;
68
+ }
69
+
70
+ debug(
71
+ "applying migration #%d of module %s",
72
+ migration.id,
73
+ migration.module,
74
+ );
75
+ await db.exec(ctx, sql.rawStatement(migration.sql));
76
+
77
+ await db.run(
78
+ ctx,
79
+ sql
80
+ .insert()
81
+ .into("t_migrations")
82
+ .values([
83
+ {
84
+ module: migration.module,
85
+ id: migration.id,
86
+ name: migration.name,
87
+ run_at: new Date(),
88
+ },
89
+ ]),
90
+ );
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ async function readMigrationFiles(db: DB, module: ModuleDefinition) {
97
+ const assetsDir = module.assetsDir;
98
+ if (!assetsDir) {
99
+ return [];
100
+ }
101
+ try {
102
+ const dbSpecificPath = FS.join(assetsDir, "migrations", db.name);
103
+ debug("reading files from %s", dbSpecificPath);
104
+ const filenames = await FS.readDirRecursively(dbSpecificPath);
105
+ return filenames.map((filename) => ({
106
+ filename,
107
+ absolutePath: FS.join(assetsDir, "migrations", db.name, filename),
108
+ }));
109
+ } catch (e) {
110
+ try {
111
+ const commonPath = FS.join(assetsDir, "migrations");
112
+ debug("reading files from %s", commonPath);
113
+ const filenames = await FS.readDirRecursively(commonPath);
114
+ return filenames.map((filename) => ({
115
+ filename,
116
+ absolutePath: FS.join(assetsDir, "migrations", filename),
117
+ }));
118
+ } catch (e) {
119
+ return [];
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,3 @@
1
+ export function snakeToCamelCase(str: string) {
2
+ return str.replace(/_([a-z])/g, (letter) => letter[1].toUpperCase());
3
+ }
@@ -0,0 +1,8 @@
1
+ export function sortBy<T>(array: T[], field: keyof T, field2?: keyof T): T[] {
2
+ return array.sort((a, b) => {
3
+ if (a[field] === b[field] && field2) {
4
+ return a[field2] < b[field2] ? -1 : 1;
5
+ }
6
+ return a[field] < b[field] ? -1 : 1;
7
+ });
8
+ }