@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.
- package/LICENSE +22 -0
- package/dist/App.d.ts +14 -0
- package/dist/App.js +227 -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 +32 -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 +24 -0
- package/dist/HttpContext.js +10 -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 +48 -0
- package/dist/Repository.js +133 -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 +15 -0
- package/dist/ViewRenderer.js +58 -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 +31 -0
- package/dist/index.js +31 -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 +54 -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 +28 -2
- package/src/App.ts +302 -0
- package/src/Component.ts +166 -0
- package/src/ConfigService.ts +121 -0
- package/src/Context.ts +49 -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 +33 -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 +204 -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 +57 -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 +81 -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
|
@@ -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,24 @@
|
|
|
1
|
+
import { type Context } from "./Context.js";
|
|
2
|
+
import { type HttpMethod } from "./Router.js";
|
|
3
|
+
export interface HttpContext<Payload = any, PathParams = any, QueryParams = any> extends Context {
|
|
4
|
+
method: HttpMethod;
|
|
5
|
+
payload: Payload;
|
|
6
|
+
path: string;
|
|
7
|
+
pathParams: PathParams;
|
|
8
|
+
query: QueryParams;
|
|
9
|
+
headers: Headers;
|
|
10
|
+
cookies: Record<string, string>;
|
|
11
|
+
abortSignal: AbortSignal;
|
|
12
|
+
responseHeaders: Headers;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
adminSessionId?: string;
|
|
15
|
+
render: (view: string | string[], data?: Record<string, any>) => Promise<Response>;
|
|
16
|
+
redirect(path: string, type?: HttpRedirectCode): Response;
|
|
17
|
+
}
|
|
18
|
+
export declare enum HttpRedirectCode {
|
|
19
|
+
HTTP_301_MOVED_PERMANENTLY = 301,
|
|
20
|
+
HTTP_302_FOUND = 302,
|
|
21
|
+
HTTP_303_SEE_OTHER = 303,
|
|
22
|
+
HTTP_307_TEMPORARY_REDIRECT = 307,
|
|
23
|
+
HTTP_308_PERMANENT_REDIRECT = 308
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {} from "./Context.js";
|
|
2
|
+
import {} from "./Router.js";
|
|
3
|
+
export var HttpRedirectCode;
|
|
4
|
+
(function (HttpRedirectCode) {
|
|
5
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_301_MOVED_PERMANENTLY"] = 301] = "HTTP_301_MOVED_PERMANENTLY";
|
|
6
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_302_FOUND"] = 302] = "HTTP_302_FOUND";
|
|
7
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_303_SEE_OTHER"] = 303] = "HTTP_303_SEE_OTHER";
|
|
8
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_307_TEMPORARY_REDIRECT"] = 307] = "HTTP_307_TEMPORARY_REDIRECT";
|
|
9
|
+
HttpRedirectCode[HttpRedirectCode["HTTP_308_PERMANENT_REDIRECT"] = 308] = "HTTP_308_PERMANENT_REDIRECT";
|
|
10
|
+
})(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,48 @@
|
|
|
1
|
+
import { Component, INJECT } from "./Component.js";
|
|
2
|
+
import { DB } from "./DB.js";
|
|
3
|
+
import type { AdminUserId, Context, UserId } from "./Context.js";
|
|
4
|
+
import { Statement } from "./utils/sql.js";
|
|
5
|
+
export declare abstract class Repository<ID, T extends Record<string, any>> extends Component {
|
|
6
|
+
protected readonly db: DB;
|
|
7
|
+
static [INJECT]: (typeof DB)[];
|
|
8
|
+
protected abstract tableName: string;
|
|
9
|
+
protected idField: string | string[];
|
|
10
|
+
protected dateFields: string[];
|
|
11
|
+
constructor(db: DB);
|
|
12
|
+
findById(ctx: Context, id: ID): Promise<T | undefined>;
|
|
13
|
+
private idClause;
|
|
14
|
+
save(ctx: Context, entity: Partial<T>): Promise<T>;
|
|
15
|
+
startTransaction(ctx: Context, fn: () => Promise<void>): void | Promise<void>;
|
|
16
|
+
protected one(ctx: Context, query: Statement): Promise<T | undefined>;
|
|
17
|
+
protected all(ctx: Context, query: Statement): Promise<T[]>;
|
|
18
|
+
protected count(ctx: Context, countQuery: Statement): Promise<number>;
|
|
19
|
+
protected toRow(entity: Partial<T>): Record<string, any>;
|
|
20
|
+
protected toEntity(row: Record<string, any>): T;
|
|
21
|
+
protected onBeforeInsert(ctx: Context, entity: Partial<T>): void;
|
|
22
|
+
protected onBeforeUpdate(ctx: Context, entity: Partial<T>): void;
|
|
23
|
+
}
|
|
24
|
+
export interface AuditedEntity {
|
|
25
|
+
createdBy?: UserId;
|
|
26
|
+
createdAt?: Date;
|
|
27
|
+
updatedBy?: UserId;
|
|
28
|
+
updatedAt?: Date;
|
|
29
|
+
}
|
|
30
|
+
export declare abstract class AuditedRepository<ID, T extends AuditedEntity> extends Repository<ID, T> {
|
|
31
|
+
protected dateFields: string[];
|
|
32
|
+
protected onBeforeInsert(ctx: Context, entity: Partial<T>): void;
|
|
33
|
+
protected onBeforeUpdate(ctx: Context, entity: Partial<T>): void;
|
|
34
|
+
}
|
|
35
|
+
export interface AdminAuditedEntity {
|
|
36
|
+
createdBy?: AdminUserId;
|
|
37
|
+
createdAt?: Date;
|
|
38
|
+
updatedBy?: AdminUserId;
|
|
39
|
+
updatedAt?: Date;
|
|
40
|
+
}
|
|
41
|
+
export declare abstract class AdminAuditedRepository<ID, T extends AdminAuditedEntity> extends Repository<ID, T> {
|
|
42
|
+
protected dateFields: string[];
|
|
43
|
+
protected onBeforeInsert(ctx: Context, entity: Partial<T>): void;
|
|
44
|
+
protected onBeforeUpdate(ctx: Context, entity: Partial<T>): void;
|
|
45
|
+
}
|
|
46
|
+
export interface Page<T> {
|
|
47
|
+
items: T[];
|
|
48
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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 Repository extends Component {
|
|
7
|
+
db;
|
|
8
|
+
static [INJECT] = [DB];
|
|
9
|
+
idField = "id";
|
|
10
|
+
dateFields = [];
|
|
11
|
+
constructor(db) {
|
|
12
|
+
super();
|
|
13
|
+
this.db = db;
|
|
14
|
+
}
|
|
15
|
+
findById(ctx, id) {
|
|
16
|
+
const query = sql.select().from(this.tableName).where(this.idClause(id));
|
|
17
|
+
return this.one(ctx, query);
|
|
18
|
+
}
|
|
19
|
+
idClause(id) {
|
|
20
|
+
if (Array.isArray(this.idField)) {
|
|
21
|
+
return this.idField.reduce((acc, k) => {
|
|
22
|
+
// @ts-expect-error
|
|
23
|
+
acc[camelToSnakeCase(k)] = id[k];
|
|
24
|
+
return acc;
|
|
25
|
+
}, {});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return {
|
|
29
|
+
[camelToSnakeCase(this.idField)]: id,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async save(ctx, entity) {
|
|
34
|
+
const idFields = Array.isArray(this.idField)
|
|
35
|
+
? this.idField
|
|
36
|
+
: [this.idField];
|
|
37
|
+
const isIDSpecified = idFields.every((idField) => entity[idField] !== undefined);
|
|
38
|
+
if (isIDSpecified) {
|
|
39
|
+
const idClause = {};
|
|
40
|
+
idFields.forEach((idField) => {
|
|
41
|
+
idClause[camelToSnakeCase(idField)] = entity[idField];
|
|
42
|
+
});
|
|
43
|
+
this.onBeforeUpdate(ctx, entity);
|
|
44
|
+
const { affectedRows } = await this.db.run(ctx, sql.update(this.tableName).set(this.toRow(entity)).where(idClause));
|
|
45
|
+
if (affectedRows > 1) {
|
|
46
|
+
throw "unexpected number of updated rows";
|
|
47
|
+
}
|
|
48
|
+
else if (affectedRows === 1) {
|
|
49
|
+
return entity;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.onBeforeInsert(ctx, entity);
|
|
53
|
+
const rows = await this.db.query(ctx, sql
|
|
54
|
+
.insert()
|
|
55
|
+
.into(this.tableName)
|
|
56
|
+
.values([this.toRow(entity)])
|
|
57
|
+
.returning(idFields.map((idField) => camelToSnakeCase(idField))));
|
|
58
|
+
if (rows.length !== 1) {
|
|
59
|
+
throw "unexpected number of inserted rows";
|
|
60
|
+
}
|
|
61
|
+
Object.assign(entity, this.toEntity(rows[0]));
|
|
62
|
+
return entity;
|
|
63
|
+
}
|
|
64
|
+
startTransaction(ctx, fn) {
|
|
65
|
+
return this.db.startTransaction(ctx, fn);
|
|
66
|
+
}
|
|
67
|
+
async one(ctx, query) {
|
|
68
|
+
const rows = await this.db.query(ctx, query);
|
|
69
|
+
return rows.length === 1 ? this.toEntity(rows[0]) : undefined;
|
|
70
|
+
}
|
|
71
|
+
async all(ctx, query) {
|
|
72
|
+
const rows = await this.db.query(ctx, query);
|
|
73
|
+
return rows.map((row) => this.toEntity(row));
|
|
74
|
+
}
|
|
75
|
+
async count(ctx, countQuery) {
|
|
76
|
+
const rows = await this.db.query(ctx, countQuery);
|
|
77
|
+
return parseInt(rows[0].count, 10);
|
|
78
|
+
}
|
|
79
|
+
toRow(entity) {
|
|
80
|
+
const row = {};
|
|
81
|
+
Object.keys(entity).forEach((key) => {
|
|
82
|
+
row[camelToSnakeCase(key)] = entity[key];
|
|
83
|
+
});
|
|
84
|
+
return row;
|
|
85
|
+
}
|
|
86
|
+
toEntity(row) {
|
|
87
|
+
const entity = {};
|
|
88
|
+
Object.keys(row).forEach((key) => {
|
|
89
|
+
const fieldName = snakeToCamelCase(key);
|
|
90
|
+
let fieldValue = row[key];
|
|
91
|
+
if (this.dateFields.includes(fieldName) &&
|
|
92
|
+
typeof fieldValue === "number") {
|
|
93
|
+
fieldValue = new Date(fieldValue);
|
|
94
|
+
}
|
|
95
|
+
entity[fieldName] = fieldValue;
|
|
96
|
+
});
|
|
97
|
+
return entity;
|
|
98
|
+
}
|
|
99
|
+
onBeforeInsert(ctx, entity) { }
|
|
100
|
+
onBeforeUpdate(ctx, entity) { }
|
|
101
|
+
}
|
|
102
|
+
export class AuditedRepository extends Repository {
|
|
103
|
+
dateFields = ["createdAt", "updatedAt"];
|
|
104
|
+
onBeforeInsert(ctx, entity) {
|
|
105
|
+
entity.createdAt = ctx.startedAt;
|
|
106
|
+
entity.updatedAt = ctx.startedAt;
|
|
107
|
+
if (ctx.user) {
|
|
108
|
+
entity.createdBy = ctx.user.id;
|
|
109
|
+
entity.updatedBy = ctx.user.id;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
onBeforeUpdate(ctx, entity) {
|
|
113
|
+
entity.updatedAt = ctx.startedAt;
|
|
114
|
+
if (ctx.user) {
|
|
115
|
+
entity.updatedBy = ctx.user.id;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class AdminAuditedRepository extends Repository {
|
|
120
|
+
dateFields = ["createdAt", "updatedAt"];
|
|
121
|
+
onBeforeInsert(ctx, entity) {
|
|
122
|
+
entity.createdAt = ctx.startedAt;
|
|
123
|
+
if (ctx.admin) {
|
|
124
|
+
entity.createdBy = ctx.admin.id;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
onBeforeUpdate(ctx, entity) {
|
|
128
|
+
entity.updatedAt = ctx.startedAt;
|
|
129
|
+
if (ctx.admin) {
|
|
130
|
+
entity.updatedBy = ctx.admin.id;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { compileTemplate } from "./contrib/template.js";
|
|
2
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
3
|
+
import { createDebug } from "./utils/createDebug.js";
|
|
4
|
+
import { Component, INJECT } from "./Component.js";
|
|
5
|
+
import { FS } from "./utils/fs.js";
|
|
6
|
+
import { isProduction } from "./utils/isProduction.js";
|
|
7
|
+
const debug = createDebug("TemplateService");
|
|
8
|
+
export class TemplateService extends Component {
|
|
9
|
+
}
|
|
10
|
+
export class FileBasedTemplateService extends TemplateService {
|
|
11
|
+
modules;
|
|
12
|
+
static [INJECT] = [ModuleDefinitions];
|
|
13
|
+
templates = new Map();
|
|
14
|
+
constructor(modules) {
|
|
15
|
+
super();
|
|
16
|
+
this.modules = modules;
|
|
17
|
+
this.modules = modules;
|
|
18
|
+
}
|
|
19
|
+
init() {
|
|
20
|
+
return this.loadTemplates();
|
|
21
|
+
}
|
|
22
|
+
async loadTemplates() {
|
|
23
|
+
for (const module of this.modules.modules) {
|
|
24
|
+
if (!module.assetsDir) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
for (const filename of await FS.readDirRecursively(FS.join(module.assetsDir, "templates"))) {
|
|
29
|
+
const fileExtension = filename.slice(filename.lastIndexOf("."));
|
|
30
|
+
if (fileExtension === this.fileExtension) {
|
|
31
|
+
const viewName = filename.slice(0, filename.lastIndexOf("."));
|
|
32
|
+
debug("adding template %s", viewName);
|
|
33
|
+
if (this.templates.has(viewName)) {
|
|
34
|
+
debug("overriding existing template %s", viewName);
|
|
35
|
+
}
|
|
36
|
+
this.templates.set(viewName, FS.join(module.assetsDir, "templates", filename));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) { }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
canRender(templateName) {
|
|
44
|
+
return this.templates.has(templateName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class BaseTemplateService extends FileBasedTemplateService {
|
|
48
|
+
fileExtension = ".html";
|
|
49
|
+
// TODO use a LRU cache
|
|
50
|
+
compiledTemplates = new Map();
|
|
51
|
+
render(templateName, data) {
|
|
52
|
+
return this.getTemplate(templateName).then((template) => template(data));
|
|
53
|
+
}
|
|
54
|
+
async getTemplate(templateName) {
|
|
55
|
+
if (isProduction && this.compiledTemplates.has(templateName)) {
|
|
56
|
+
return this.compiledTemplates.get(templateName);
|
|
57
|
+
}
|
|
58
|
+
const absolutePath = this.templates.get(templateName);
|
|
59
|
+
const content = await FS.readFile(absolutePath);
|
|
60
|
+
const compiledTemplate = compileTemplate(content);
|
|
61
|
+
if (isProduction) {
|
|
62
|
+
this.compiledTemplates.set(templateName, compiledTemplate);
|
|
63
|
+
}
|
|
64
|
+
return compiledTemplate;
|
|
65
|
+
}
|
|
66
|
+
}
|