@tymber/common 0.0.1-alpha.0 → 0.1.0
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/LICENSE +22 -0
- package/README.md +46 -0
- package/dist/App.d.ts +17 -0
- package/dist/App.js +236 -0
- package/dist/Component.d.ts +16 -0
- package/dist/Component.js +116 -0
- package/dist/ConfigService.d.ts +31 -0
- package/dist/ConfigService.js +75 -0
- package/dist/Context.d.ts +41 -0
- package/dist/Context.js +10 -0
- package/dist/DB.d.ts +15 -0
- package/dist/DB.js +7 -0
- package/dist/Endpoint.d.ts +21 -0
- package/dist/Endpoint.js +87 -0
- package/dist/EventEmitter.d.ts +9 -0
- package/dist/EventEmitter.js +21 -0
- package/dist/Handler.d.ts +8 -0
- package/dist/Handler.js +8 -0
- package/dist/HttpContext.d.ts +26 -0
- package/dist/HttpContext.js +11 -0
- package/dist/I18nService.d.ts +18 -0
- package/dist/I18nService.js +72 -0
- package/dist/Middleware.d.ts +6 -0
- package/dist/Middleware.js +4 -0
- package/dist/Module.d.ts +47 -0
- package/dist/Module.js +12 -0
- package/dist/PubSubService.d.ts +20 -0
- package/dist/PubSubService.js +60 -0
- package/dist/Repository.d.ts +29 -0
- package/dist/Repository.js +110 -0
- package/dist/Router.d.ts +10 -0
- package/dist/Router.js +53 -0
- package/dist/TemplateService.d.ts +22 -0
- package/dist/TemplateService.js +66 -0
- package/dist/View.d.ts +17 -0
- package/dist/View.js +48 -0
- package/dist/ViewRenderer.d.ts +16 -0
- package/dist/ViewRenderer.js +59 -0
- package/dist/contrib/accept-language-parser.d.ts +9 -0
- package/dist/contrib/accept-language-parser.js +73 -0
- package/dist/contrib/cookie.d.ts +33 -0
- package/dist/contrib/cookie.js +207 -0
- package/dist/contrib/template.d.ts +1 -0
- package/dist/contrib/template.js +107 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +32 -0
- package/dist/utils/ajv.d.ts +2 -0
- package/dist/utils/ajv.js +10 -0
- package/dist/utils/camelToSnakeCase.d.ts +1 -0
- package/dist/utils/camelToSnakeCase.js +3 -0
- package/dist/utils/computeBaseUrl.d.ts +1 -0
- package/dist/utils/computeBaseUrl.js +37 -0
- package/dist/utils/computeContentType.d.ts +1 -0
- package/dist/utils/computeContentType.js +17 -0
- package/dist/utils/createDebug.d.ts +1 -0
- package/dist/utils/createDebug.js +4 -0
- package/dist/utils/createTestApp.d.ts +8 -0
- package/dist/utils/createTestApp.js +57 -0
- package/dist/utils/escapeValue.d.ts +1 -0
- package/dist/utils/escapeValue.js +3 -0
- package/dist/utils/fs.d.ts +6 -0
- package/dist/utils/fs.js +13 -0
- package/dist/utils/isAdmin.d.ts +2 -0
- package/dist/utils/isAdmin.js +4 -0
- package/dist/utils/isProduction.d.ts +1 -0
- package/dist/utils/isProduction.js +1 -0
- package/dist/utils/loadModules.d.ts +3 -0
- package/dist/utils/loadModules.js +83 -0
- package/dist/utils/randomId.d.ts +1 -0
- package/dist/utils/randomId.js +4 -0
- package/dist/utils/randomUUID.d.ts +1 -0
- package/dist/utils/randomUUID.js +4 -0
- package/dist/utils/runMigrations.d.ts +3 -0
- package/dist/utils/runMigrations.js +85 -0
- package/dist/utils/snakeToCamelCase.d.ts +1 -0
- package/dist/utils/snakeToCamelCase.js +3 -0
- package/dist/utils/sortBy.d.ts +1 -0
- package/dist/utils/sortBy.js +8 -0
- package/dist/utils/sql.d.ts +120 -0
- package/dist/utils/sql.js +433 -0
- package/dist/utils/toNodeHandler.d.ts +2 -0
- package/dist/utils/toNodeHandler.js +91 -0
- package/dist/utils/types.d.ts +3 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/waitFor.d.ts +1 -0
- package/dist/utils/waitFor.js +5 -0
- package/package.json +33 -2
- package/src/App.ts +319 -0
- package/src/Component.ts +166 -0
- package/src/ConfigService.ts +121 -0
- package/src/Context.ts +60 -0
- package/src/DB.ts +28 -0
- package/src/Endpoint.ts +118 -0
- package/src/EventEmitter.ts +32 -0
- package/src/Handler.ts +14 -0
- package/src/HttpContext.ts +35 -0
- package/src/I18nService.ts +96 -0
- package/src/Middleware.ts +10 -0
- package/src/Module.ts +60 -0
- package/src/PubSubService.ts +77 -0
- package/src/Repository.ts +158 -0
- package/src/Router.ts +77 -0
- package/src/TemplateService.ts +97 -0
- package/src/View.ts +60 -0
- package/src/ViewRenderer.ts +71 -0
- package/src/contrib/accept-language-parser.ts +94 -0
- package/src/contrib/cookie.ts +256 -0
- package/src/contrib/template.ts +134 -0
- package/src/index.ts +54 -0
- package/src/utils/ajv.ts +13 -0
- package/src/utils/camelToSnakeCase.ts +3 -0
- package/src/utils/computeBaseUrl.ts +46 -0
- package/src/utils/computeContentType.ts +17 -0
- package/src/utils/createDebug.ts +5 -0
- package/src/utils/createTestApp.ts +84 -0
- package/src/utils/escapeValue.ts +3 -0
- package/src/utils/fs.ts +15 -0
- package/src/utils/isAdmin.ts +5 -0
- package/src/utils/isProduction.ts +2 -0
- package/src/utils/loadModules.ts +105 -0
- package/src/utils/randomId.ts +5 -0
- package/src/utils/randomUUID.ts +5 -0
- package/src/utils/runMigrations.ts +122 -0
- package/src/utils/snakeToCamelCase.ts +3 -0
- package/src/utils/sortBy.ts +8 -0
- package/src/utils/sql.ts +553 -0
- package/src/utils/toNodeHandler.ts +121 -0
- package/src/utils/types.ts +1 -0
- package/src/utils/waitFor.ts +5 -0
package/dist/Endpoint.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {} from "./HttpContext.js";
|
|
2
|
+
import {} from "ajv";
|
|
3
|
+
import { Handler } from "./Handler.js";
|
|
4
|
+
import { AJV_INSTANCE } from "./utils/ajv.js";
|
|
5
|
+
function formatErrors(errors) {
|
|
6
|
+
return errors.map((error) => ({
|
|
7
|
+
keyword: error.keyword,
|
|
8
|
+
message: error.message,
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
class BaseEndpoint extends Handler {
|
|
12
|
+
pathParamsSchema; // JSONSchemaType<Params>;
|
|
13
|
+
querySchema; // JSONSchemaType<Query>;
|
|
14
|
+
payloadSchema; // JSONSchemaType<Payload>;
|
|
15
|
+
validatePathParams;
|
|
16
|
+
validateQuery;
|
|
17
|
+
validatePayload;
|
|
18
|
+
doHandle(ctx) {
|
|
19
|
+
const { pathParams, query, payload } = ctx;
|
|
20
|
+
if (this.pathParamsSchema && !this.validatePathParams) {
|
|
21
|
+
this.validatePathParams = AJV_INSTANCE.compile(this.pathParamsSchema);
|
|
22
|
+
}
|
|
23
|
+
if (this.validatePathParams && !this.validatePathParams(pathParams)) {
|
|
24
|
+
return this.badRequest("invalid path params", {
|
|
25
|
+
errors: formatErrors(this.validatePathParams.errors),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (this.querySchema && !this.validateQuery) {
|
|
29
|
+
this.validateQuery = AJV_INSTANCE.compile(this.querySchema);
|
|
30
|
+
}
|
|
31
|
+
if (this.validateQuery && !this.validateQuery(query)) {
|
|
32
|
+
return this.badRequest("invalid query params", {
|
|
33
|
+
errors: formatErrors(this.validateQuery.errors),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (this.payloadSchema && !this.validatePayload) {
|
|
37
|
+
this.validatePayload = AJV_INSTANCE.compile(this.payloadSchema);
|
|
38
|
+
}
|
|
39
|
+
if (this.validatePayload && !this.validatePayload(payload)) {
|
|
40
|
+
return this.badRequest("invalid payload", {
|
|
41
|
+
errors: formatErrors(this.validatePayload.errors),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!this.hasPermission(ctx)) {
|
|
45
|
+
return Response.json({
|
|
46
|
+
message: "forbidden",
|
|
47
|
+
}, {
|
|
48
|
+
status: 403,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return this.handle(ctx);
|
|
52
|
+
}
|
|
53
|
+
badRequest(message, details) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
message,
|
|
56
|
+
...details,
|
|
57
|
+
}, {
|
|
58
|
+
status: 400,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class Endpoint extends BaseEndpoint {
|
|
63
|
+
_endpointBrand; // nominal typing
|
|
64
|
+
doHandle(ctx) {
|
|
65
|
+
if (!this.allowAnonymous && !ctx.user) {
|
|
66
|
+
return Response.json({
|
|
67
|
+
message: "unauthorized",
|
|
68
|
+
}, {
|
|
69
|
+
status: 401,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return super.doHandle(ctx);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export class AdminEndpoint extends BaseEndpoint {
|
|
76
|
+
_adminEndpointBrand; // nominal typing
|
|
77
|
+
doHandle(ctx) {
|
|
78
|
+
if (!this.allowAnonymous && !ctx.admin) {
|
|
79
|
+
return Response.json({
|
|
80
|
+
message: "unauthorized",
|
|
81
|
+
}, {
|
|
82
|
+
status: 401,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return super.doHandle(ctx);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type EventMap = Record<string, any>;
|
|
2
|
+
type EventKey<T extends EventMap> = string & keyof T;
|
|
3
|
+
type EventCallback<T> = (payload: T) => void;
|
|
4
|
+
export declare class EventEmitter<Events extends EventMap> {
|
|
5
|
+
private listeners;
|
|
6
|
+
on<K extends EventKey<Events>>(event: K, callback: EventCallback<Events[K]>): void;
|
|
7
|
+
emit<K extends EventKey<Events>>(event: K, payload: Events[K]): void;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createDebug } from "./utils/createDebug.js";
|
|
2
|
+
const debug = createDebug("EventEmitter");
|
|
3
|
+
export class EventEmitter {
|
|
4
|
+
listeners = new Map();
|
|
5
|
+
on(event, callback) {
|
|
6
|
+
debug("subscribing to event %s", event);
|
|
7
|
+
if (!this.listeners.has(event)) {
|
|
8
|
+
this.listeners.set(event, []);
|
|
9
|
+
}
|
|
10
|
+
this.listeners
|
|
11
|
+
.get(event)
|
|
12
|
+
.push(callback);
|
|
13
|
+
}
|
|
14
|
+
emit(event, payload) {
|
|
15
|
+
debug("emitting event %s", event);
|
|
16
|
+
const callbacks = this.listeners.get(event);
|
|
17
|
+
if (callbacks) {
|
|
18
|
+
callbacks.forEach((callback) => callback(payload));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Component } from "./Component.js";
|
|
2
|
+
import { type HttpContext } from "./HttpContext.js";
|
|
3
|
+
export declare abstract class Handler extends Component {
|
|
4
|
+
protected allowAnonymous: boolean;
|
|
5
|
+
protected hasPermission(ctx: HttpContext): boolean;
|
|
6
|
+
abstract doHandle(ctx: HttpContext): Response | Promise<Response>;
|
|
7
|
+
protected abstract handle(ctx: HttpContext): Response | Promise<Response>;
|
|
8
|
+
}
|
package/dist/Handler.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Context } from "./Context.js";
|
|
2
|
+
import { type HttpMethod } from "./Router.js";
|
|
3
|
+
import { type Locale } from "./contrib/accept-language-parser.js";
|
|
4
|
+
export interface HttpContext<Payload = any, PathParams = any, QueryParams = any> extends Context {
|
|
5
|
+
method: HttpMethod;
|
|
6
|
+
payload: Payload;
|
|
7
|
+
path: string;
|
|
8
|
+
pathParams: PathParams;
|
|
9
|
+
query: QueryParams;
|
|
10
|
+
headers: Headers;
|
|
11
|
+
cookies: Record<string, string>;
|
|
12
|
+
abortSignal: AbortSignal;
|
|
13
|
+
locale: Locale;
|
|
14
|
+
responseHeaders: Headers;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
adminSessionId?: string;
|
|
17
|
+
render: (view: string | string[], data?: Record<string, any>) => Promise<Response>;
|
|
18
|
+
redirect(path: string, type?: HttpRedirectCode): Response;
|
|
19
|
+
}
|
|
20
|
+
export declare enum HttpRedirectCode {
|
|
21
|
+
HTTP_301_MOVED_PERMANENTLY = 301,
|
|
22
|
+
HTTP_302_FOUND = 302,
|
|
23
|
+
HTTP_303_SEE_OTHER = 303,
|
|
24
|
+
HTTP_307_TEMPORARY_REDIRECT = 307,
|
|
25
|
+
HTTP_308_PERMANENT_REDIRECT = 308
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {} from "./Context.js";
|
|
2
|
+
import {} from "./Router.js";
|
|
3
|
+
import {} from "./contrib/accept-language-parser.js";
|
|
4
|
+
export var HttpRedirectCode;
|
|
5
|
+
(function (HttpRedirectCode) {
|
|
6
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_301_MOVED_PERMANENTLY"] = 301] = "HTTP_301_MOVED_PERMANENTLY";
|
|
7
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_302_FOUND"] = 302] = "HTTP_302_FOUND";
|
|
8
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_303_SEE_OTHER"] = 303] = "HTTP_303_SEE_OTHER";
|
|
9
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_307_TEMPORARY_REDIRECT"] = 307] = "HTTP_307_TEMPORARY_REDIRECT";
|
|
10
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_308_PERMANENT_REDIRECT"] = 308] = "HTTP_308_PERMANENT_REDIRECT";
|
|
11
|
+
})(HttpRedirectCode || (HttpRedirectCode = {}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Locale } from "./contrib/accept-language-parser.js";
|
|
2
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
3
|
+
import { Component, INJECT } from "./Component.js";
|
|
4
|
+
import type { Context } from "./Context.js";
|
|
5
|
+
export declare abstract class I18nService extends Component {
|
|
6
|
+
abstract availableLocales(): Locale[];
|
|
7
|
+
abstract translate(ctx: Context, locale: Locale, key: string, ...args: any[]): string;
|
|
8
|
+
}
|
|
9
|
+
export declare class BaseI18nService extends I18nService {
|
|
10
|
+
private readonly modules;
|
|
11
|
+
static [INJECT]: (typeof ModuleDefinitions)[];
|
|
12
|
+
private readonly translations;
|
|
13
|
+
constructor(modules: ModuleDefinitions);
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
private loadTranslations;
|
|
16
|
+
availableLocales(): Locale[];
|
|
17
|
+
translate(_ctx: Context, locale: Locale, key: string, ...args: any[]): string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {} from "./contrib/accept-language-parser.js";
|
|
2
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
3
|
+
import { compileTemplate } from "./contrib/template.js";
|
|
4
|
+
import { Component, INJECT } from "./Component.js";
|
|
5
|
+
import { FS } from "./utils/fs.js";
|
|
6
|
+
function flatten(obj) {
|
|
7
|
+
const output = {};
|
|
8
|
+
function flattenRecursively(curr, parentKey) {
|
|
9
|
+
if (typeof curr === "object" && curr !== null && !Array.isArray(curr)) {
|
|
10
|
+
for (const key in curr) {
|
|
11
|
+
flattenRecursively(curr[key], parentKey ? `${parentKey}.${key}` : key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
output[parentKey] = curr;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
flattenRecursively(obj, "");
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
export class I18nService extends Component {
|
|
22
|
+
}
|
|
23
|
+
export class BaseI18nService extends I18nService {
|
|
24
|
+
modules;
|
|
25
|
+
static [INJECT] = [ModuleDefinitions];
|
|
26
|
+
translations = new Map();
|
|
27
|
+
constructor(modules) {
|
|
28
|
+
super();
|
|
29
|
+
this.modules = modules;
|
|
30
|
+
}
|
|
31
|
+
init() {
|
|
32
|
+
return this.loadTranslations();
|
|
33
|
+
}
|
|
34
|
+
async loadTranslations() {
|
|
35
|
+
try {
|
|
36
|
+
for (const module of this.modules.modules) {
|
|
37
|
+
if (!module.assetsDir) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const filename of await FS.readDirRecursively(FS.join(module.assetsDir, "i18n"))) {
|
|
41
|
+
if (!filename.endsWith(".json")) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const content = await FS.readFile(FS.join(module.assetsDir, "i18n", filename));
|
|
45
|
+
const locale = filename.slice(0, -5);
|
|
46
|
+
const values = JSON.parse(content);
|
|
47
|
+
let localizedTranslations = this.translations.get(locale);
|
|
48
|
+
if (!localizedTranslations) {
|
|
49
|
+
this.translations.set(locale, (localizedTranslations = new Map()));
|
|
50
|
+
}
|
|
51
|
+
const flatTranslations = flatten(values);
|
|
52
|
+
for (const [key, value] of Object.entries(flatTranslations)) {
|
|
53
|
+
localizedTranslations.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) { }
|
|
59
|
+
}
|
|
60
|
+
availableLocales() {
|
|
61
|
+
return Array.from(this.translations.keys());
|
|
62
|
+
}
|
|
63
|
+
translate(_ctx, locale, key, ...args) {
|
|
64
|
+
const value = this.translations.get(locale)?.get(key);
|
|
65
|
+
if (value && value.includes("<%")) {
|
|
66
|
+
return compileTemplate(value)(args);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
return value || "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HttpContext } from "./HttpContext.js";
|
|
2
|
+
import { Component } from "./Component.js";
|
|
3
|
+
export declare abstract class Middleware extends Component {
|
|
4
|
+
private _middlewareBrand;
|
|
5
|
+
abstract handle(ctx: HttpContext): void | Response | Promise<void | Response>;
|
|
6
|
+
}
|
package/dist/Module.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Component, type Ctor } from "./Component.js";
|
|
2
|
+
import type { HttpMethod } from "./Router.js";
|
|
3
|
+
import { AdminEndpoint, Endpoint } from "./Endpoint.js";
|
|
4
|
+
import { AdminView, View } from "./View.js";
|
|
5
|
+
import { Middleware } from "./Middleware.js";
|
|
6
|
+
import { type Handler } from "./Handler.js";
|
|
7
|
+
export interface AdminSidebarItem {
|
|
8
|
+
label: string;
|
|
9
|
+
icon: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Module {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
assetsDir?: string;
|
|
16
|
+
adminSidebarItems?: AdminSidebarItem[];
|
|
17
|
+
init(app: AppInit): void;
|
|
18
|
+
}
|
|
19
|
+
export interface AppInit {
|
|
20
|
+
component<T extends Component>(ctor: Ctor<T>): void;
|
|
21
|
+
endpoint(method: HttpMethod, path: string, ctor: Ctor<Endpoint>): void;
|
|
22
|
+
view(path: string, ctor: Ctor<View>): void;
|
|
23
|
+
adminEndpoint(method: HttpMethod, path: string, ctor: Ctor<AdminEndpoint>): void;
|
|
24
|
+
adminView(path: string, ctor: Ctor<AdminView>): void;
|
|
25
|
+
middleware(ctor: Ctor<Middleware>): void;
|
|
26
|
+
}
|
|
27
|
+
export interface Route {
|
|
28
|
+
method: HttpMethod;
|
|
29
|
+
path: string;
|
|
30
|
+
handlerName: string;
|
|
31
|
+
handler: Handler;
|
|
32
|
+
}
|
|
33
|
+
export interface ModuleDefinition {
|
|
34
|
+
name: string;
|
|
35
|
+
version: string;
|
|
36
|
+
assetsDir?: string;
|
|
37
|
+
adminSidebarItems?: AdminSidebarItem[];
|
|
38
|
+
endpoints: Route[];
|
|
39
|
+
views: Route[];
|
|
40
|
+
adminEndpoints: Route[];
|
|
41
|
+
adminViews: Route[];
|
|
42
|
+
middlewares: Middleware[];
|
|
43
|
+
}
|
|
44
|
+
export declare class ModuleDefinitions extends Component {
|
|
45
|
+
readonly modules: ModuleDefinition[];
|
|
46
|
+
constructor(modules: ModuleDefinition[]);
|
|
47
|
+
}
|
package/dist/Module.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Component } from "./Component.js";
|
|
2
|
+
import { AdminEndpoint, Endpoint } from "./Endpoint.js";
|
|
3
|
+
import { AdminView, View } from "./View.js";
|
|
4
|
+
import { Middleware } from "./Middleware.js";
|
|
5
|
+
import {} from "./Handler.js";
|
|
6
|
+
export class ModuleDefinitions extends Component {
|
|
7
|
+
modules;
|
|
8
|
+
constructor(modules) {
|
|
9
|
+
super();
|
|
10
|
+
this.modules = modules;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Component } from "./Component.js";
|
|
2
|
+
import type { Context } from "./Context.js";
|
|
3
|
+
interface Message {
|
|
4
|
+
from: string;
|
|
5
|
+
type: string;
|
|
6
|
+
payload?: any;
|
|
7
|
+
}
|
|
8
|
+
export declare class PubSubService extends Component {
|
|
9
|
+
protected readonly id: string;
|
|
10
|
+
private eventEmitter;
|
|
11
|
+
publish(_ctx: Context, _type: string, _payload?: any): void;
|
|
12
|
+
subscribe<T>(type: string, handler: (payload: T) => void): void;
|
|
13
|
+
protected onMessage(message: Message): void;
|
|
14
|
+
}
|
|
15
|
+
export declare class NodeClusterPubSubService extends PubSubService {
|
|
16
|
+
constructor();
|
|
17
|
+
publish(_ctx: Context, type: string, payload?: any): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function initPrimary(): void;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component } from "./Component.js";
|
|
2
|
+
import { createDebug } from "./utils/createDebug.js";
|
|
3
|
+
import { EventEmitter } from "./EventEmitter.js";
|
|
4
|
+
import cluster from "node:cluster";
|
|
5
|
+
import { randomId } from "./utils/randomId.js";
|
|
6
|
+
const debug = createDebug("PubSubService");
|
|
7
|
+
export class PubSubService extends Component {
|
|
8
|
+
id = randomId();
|
|
9
|
+
eventEmitter = new EventEmitter();
|
|
10
|
+
publish(_ctx, _type, _payload) {
|
|
11
|
+
// noop
|
|
12
|
+
}
|
|
13
|
+
subscribe(type, handler) {
|
|
14
|
+
this.eventEmitter.on(type, handler);
|
|
15
|
+
}
|
|
16
|
+
onMessage(message) {
|
|
17
|
+
if (message.from === this.id) {
|
|
18
|
+
debug("ignore message from self");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
debug("received message %s", message.type);
|
|
22
|
+
this.eventEmitter.emit(message.type, message.payload);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function ignoreError() { }
|
|
26
|
+
export class NodeClusterPubSubService extends PubSubService {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
if (!cluster.isWorker) {
|
|
30
|
+
throw "not worker";
|
|
31
|
+
}
|
|
32
|
+
process.on("message", (message) => this.onMessage(message));
|
|
33
|
+
}
|
|
34
|
+
publish(_ctx, type, payload) {
|
|
35
|
+
if (typeof process.send !== "function") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
debug("sending message %s", type);
|
|
39
|
+
const message = {
|
|
40
|
+
from: this.id,
|
|
41
|
+
type,
|
|
42
|
+
payload,
|
|
43
|
+
};
|
|
44
|
+
process.send(message, undefined, {}, ignoreError);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function initPrimary() {
|
|
48
|
+
if (!cluster.isPrimary) {
|
|
49
|
+
throw "not primary";
|
|
50
|
+
}
|
|
51
|
+
cluster.on("message", (worker, message) => {
|
|
52
|
+
const emitterId = String(worker.id);
|
|
53
|
+
debug("forwarding message %s to other workers", message.type);
|
|
54
|
+
for (const workerId in cluster.workers) {
|
|
55
|
+
if (workerId !== emitterId) {
|
|
56
|
+
cluster.workers[workerId]?.send(message, ignoreError);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Component, INJECT } from "./Component.js";
|
|
2
|
+
import { DB } from "./DB.js";
|
|
3
|
+
import type { Context } from "./Context.js";
|
|
4
|
+
import { Statement } from "./utils/sql.js";
|
|
5
|
+
export declare class EntityNotFoundError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export declare abstract class Repository<ID, T extends Record<string, any>> extends Component {
|
|
8
|
+
protected readonly db: DB;
|
|
9
|
+
static [INJECT]: (typeof DB)[];
|
|
10
|
+
protected abstract tableName: string;
|
|
11
|
+
protected idField: string | string[];
|
|
12
|
+
protected dateFields: string[];
|
|
13
|
+
constructor(db: DB);
|
|
14
|
+
findById(ctx: Context, id: ID): Promise<T | undefined>;
|
|
15
|
+
deleteById(ctx: Context, id: ID): Promise<void>;
|
|
16
|
+
private idClause;
|
|
17
|
+
save(ctx: Context, entity: Partial<T>): Promise<T>;
|
|
18
|
+
startTransaction(ctx: Context, fn: () => Promise<void>): void | Promise<void>;
|
|
19
|
+
protected one(ctx: Context, query: Statement): Promise<T | undefined>;
|
|
20
|
+
protected all(ctx: Context, query: Statement): Promise<T[]>;
|
|
21
|
+
protected count(ctx: Context, countQuery: Statement): Promise<number>;
|
|
22
|
+
protected toRow(entity: Partial<T>): Record<string, any>;
|
|
23
|
+
protected toEntity(row: Record<string, any>): T;
|
|
24
|
+
protected onBeforeInsert(ctx: Context, entity: Partial<T>): void;
|
|
25
|
+
protected onBeforeUpdate(ctx: Context, entity: Partial<T>): void;
|
|
26
|
+
}
|
|
27
|
+
export interface Page<T> {
|
|
28
|
+
items: T[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { camelToSnakeCase } from "./utils/camelToSnakeCase.js";
|
|
2
|
+
import { snakeToCamelCase } from "./utils/snakeToCamelCase.js";
|
|
3
|
+
import { Component, INJECT } from "./Component.js";
|
|
4
|
+
import { DB } from "./DB.js";
|
|
5
|
+
import { sql, Statement } from "./utils/sql.js";
|
|
6
|
+
export class EntityNotFoundError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export class Repository extends Component {
|
|
9
|
+
db;
|
|
10
|
+
static [INJECT] = [DB];
|
|
11
|
+
idField = "id";
|
|
12
|
+
dateFields = [];
|
|
13
|
+
constructor(db) {
|
|
14
|
+
super();
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
findById(ctx, id) {
|
|
18
|
+
const query = sql.select().from(this.tableName).where(this.idClause(id));
|
|
19
|
+
return this.one(ctx, query);
|
|
20
|
+
}
|
|
21
|
+
async deleteById(ctx, id) {
|
|
22
|
+
const query = sql.deleteFrom(this.tableName).where(this.idClause(id));
|
|
23
|
+
const res = await this.db.run(ctx, query);
|
|
24
|
+
if (res.affectedRows !== 1) {
|
|
25
|
+
throw new EntityNotFoundError();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
idClause(id) {
|
|
29
|
+
if (Array.isArray(this.idField)) {
|
|
30
|
+
return this.idField.reduce((acc, k) => {
|
|
31
|
+
// @ts-expect-error
|
|
32
|
+
acc[camelToSnakeCase(k)] = id[k];
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return {
|
|
38
|
+
[camelToSnakeCase(this.idField)]: id,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async save(ctx, entity) {
|
|
43
|
+
const idFields = Array.isArray(this.idField)
|
|
44
|
+
? this.idField
|
|
45
|
+
: [this.idField];
|
|
46
|
+
const isIDSpecified = idFields.every((idField) => entity[idField] !== undefined);
|
|
47
|
+
if (isIDSpecified) {
|
|
48
|
+
const idClause = {};
|
|
49
|
+
idFields.forEach((idField) => {
|
|
50
|
+
idClause[camelToSnakeCase(idField)] = entity[idField];
|
|
51
|
+
});
|
|
52
|
+
this.onBeforeUpdate(ctx, entity);
|
|
53
|
+
const { affectedRows } = await this.db.run(ctx, sql.update(this.tableName).set(this.toRow(entity)).where(idClause));
|
|
54
|
+
if (affectedRows > 1) {
|
|
55
|
+
throw "unexpected number of updated rows";
|
|
56
|
+
}
|
|
57
|
+
else if (affectedRows === 1) {
|
|
58
|
+
return entity;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.onBeforeInsert(ctx, entity);
|
|
62
|
+
const rows = await this.db.query(ctx, sql
|
|
63
|
+
.insert()
|
|
64
|
+
.into(this.tableName)
|
|
65
|
+
.values([this.toRow(entity)])
|
|
66
|
+
.returning(idFields.map((idField) => camelToSnakeCase(idField))));
|
|
67
|
+
if (rows.length !== 1) {
|
|
68
|
+
throw "unexpected number of inserted rows";
|
|
69
|
+
}
|
|
70
|
+
Object.assign(entity, this.toEntity(rows[0]));
|
|
71
|
+
return entity;
|
|
72
|
+
}
|
|
73
|
+
startTransaction(ctx, fn) {
|
|
74
|
+
return this.db.startTransaction(ctx, fn);
|
|
75
|
+
}
|
|
76
|
+
async one(ctx, query) {
|
|
77
|
+
const rows = await this.db.query(ctx, query);
|
|
78
|
+
return rows.length === 1 ? this.toEntity(rows[0]) : undefined;
|
|
79
|
+
}
|
|
80
|
+
async all(ctx, query) {
|
|
81
|
+
const rows = await this.db.query(ctx, query);
|
|
82
|
+
return rows.map((row) => this.toEntity(row));
|
|
83
|
+
}
|
|
84
|
+
async count(ctx, countQuery) {
|
|
85
|
+
const rows = await this.db.query(ctx, countQuery);
|
|
86
|
+
return parseInt(rows[0].count, 10);
|
|
87
|
+
}
|
|
88
|
+
toRow(entity) {
|
|
89
|
+
const row = {};
|
|
90
|
+
Object.keys(entity).forEach((key) => {
|
|
91
|
+
row[camelToSnakeCase(key)] = entity[key];
|
|
92
|
+
});
|
|
93
|
+
return row;
|
|
94
|
+
}
|
|
95
|
+
toEntity(row) {
|
|
96
|
+
const entity = {};
|
|
97
|
+
Object.keys(row).forEach((key) => {
|
|
98
|
+
const fieldName = snakeToCamelCase(key);
|
|
99
|
+
let fieldValue = row[key];
|
|
100
|
+
if (this.dateFields.includes(fieldName) &&
|
|
101
|
+
typeof fieldValue === "number") {
|
|
102
|
+
fieldValue = new Date(fieldValue);
|
|
103
|
+
}
|
|
104
|
+
entity[fieldName] = fieldValue;
|
|
105
|
+
});
|
|
106
|
+
return entity;
|
|
107
|
+
}
|
|
108
|
+
onBeforeInsert(ctx, entity) { }
|
|
109
|
+
onBeforeUpdate(ctx, entity) { }
|
|
110
|
+
}
|
package/dist/Router.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Handler } from "./Handler.js";
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD";
|
|
3
|
+
export declare class Router {
|
|
4
|
+
private routes;
|
|
5
|
+
registerRoute(method: HttpMethod, path: string, handler: Handler): void;
|
|
6
|
+
findRoute(method: HttpMethod, path: string): {
|
|
7
|
+
handler: Handler;
|
|
8
|
+
pathParams: Record<string, string>;
|
|
9
|
+
} | undefined;
|
|
10
|
+
}
|
package/dist/Router.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Handler } from "./Handler.js";
|
|
2
|
+
function createMatcher(path) {
|
|
3
|
+
if (!path.includes(":")) {
|
|
4
|
+
return (p) => {
|
|
5
|
+
return p === path ? {} : undefined;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
const pathParams = [];
|
|
9
|
+
const regex = new RegExp("^" +
|
|
10
|
+
path.replace(/:(\w+)/g, (pathParam) => {
|
|
11
|
+
pathParams.push(pathParam.substring(1));
|
|
12
|
+
return "([\\w-_]+)";
|
|
13
|
+
}) +
|
|
14
|
+
"$");
|
|
15
|
+
return (path) => {
|
|
16
|
+
const match = regex.exec(path);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const params = {};
|
|
21
|
+
for (let i = 0; i < pathParams.length; i++) {
|
|
22
|
+
params[pathParams[i]] = match[i + 1];
|
|
23
|
+
}
|
|
24
|
+
return params;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export class Router {
|
|
28
|
+
routes = [];
|
|
29
|
+
registerRoute(method, path, handler) {
|
|
30
|
+
for (const route of this.routes) {
|
|
31
|
+
if (route.path === path) {
|
|
32
|
+
route.handlers.set(method, handler);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.routes.push({
|
|
37
|
+
path,
|
|
38
|
+
matcher: createMatcher(path),
|
|
39
|
+
handlers: new Map([[method, handler]]),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
findRoute(method, path) {
|
|
43
|
+
for (const route of this.routes) {
|
|
44
|
+
const pathParams = route.matcher(path);
|
|
45
|
+
if (pathParams !== undefined && route.handlers.has(method)) {
|
|
46
|
+
return {
|
|
47
|
+
handler: route.handlers.get(method),
|
|
48
|
+
pathParams,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
2
|
+
import { Component, INJECT } from "./Component.js";
|
|
3
|
+
export declare abstract class TemplateService extends Component {
|
|
4
|
+
abstract canRender(templateName: string): boolean;
|
|
5
|
+
abstract render(templateName: string, data: Record<string, any>): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare abstract class FileBasedTemplateService extends TemplateService {
|
|
8
|
+
private readonly modules;
|
|
9
|
+
static [INJECT]: (typeof ModuleDefinitions)[];
|
|
10
|
+
abstract readonly fileExtension: string;
|
|
11
|
+
protected templates: Map<string, string>;
|
|
12
|
+
constructor(modules: ModuleDefinitions);
|
|
13
|
+
init(): Promise<void>;
|
|
14
|
+
private loadTemplates;
|
|
15
|
+
canRender(templateName: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class BaseTemplateService extends FileBasedTemplateService {
|
|
18
|
+
fileExtension: string;
|
|
19
|
+
private compiledTemplates;
|
|
20
|
+
render(templateName: string, data: Record<string, any>): Promise<string>;
|
|
21
|
+
private getTemplate;
|
|
22
|
+
}
|