@tymber/common 0.0.1 → 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/README.md +46 -0
- package/dist/App.d.ts +5 -2
- package/dist/App.js +13 -4
- package/dist/Context.d.ts +15 -6
- package/dist/HttpContext.d.ts +2 -0
- package/dist/HttpContext.js +1 -0
- package/dist/Repository.d.ts +4 -23
- package/dist/Repository.js +9 -32
- package/dist/ViewRenderer.d.ts +1 -0
- package/dist/ViewRenderer.js +4 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/utils/createTestApp.js +4 -1
- package/package.json +7 -2
- package/src/App.ts +23 -6
- package/src/Context.ts +17 -6
- package/src/HttpContext.ts +2 -0
- package/src/Repository.ts +13 -59
- package/src/ViewRenderer.ts +8 -8
- package/src/index.ts +7 -10
- package/src/utils/createTestApp.ts +4 -1
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<h1>Common module of the Tymber framework</h1>
|
|
2
|
+
|
|
3
|
+
The internals of the framework.
|
|
4
|
+
|
|
5
|
+
**Table of contents**
|
|
6
|
+
|
|
7
|
+
<!-- TOC -->
|
|
8
|
+
* [Installation](#installation)
|
|
9
|
+
* [Usage](#usage)
|
|
10
|
+
* [License](#license)
|
|
11
|
+
<!-- TOC -->
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm i @tymber/common
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import * as pg from "pg";
|
|
23
|
+
import { PostgresDB } from "@tymber/postgres";
|
|
24
|
+
import { App, toNodeHandler } from "@tymber/common";
|
|
25
|
+
import { CoreModule } from "@tymber/core";
|
|
26
|
+
import { createServer } from "node:http";
|
|
27
|
+
|
|
28
|
+
const pgPool = new pg.Pool({
|
|
29
|
+
user: "postgres",
|
|
30
|
+
password: "changeit",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const db = new PostgresDB(pgPool);
|
|
34
|
+
|
|
35
|
+
const app = await App.create(db, [
|
|
36
|
+
CoreModule
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const httpServer = createServer(toNodeHandler(app.fetch));
|
|
40
|
+
|
|
41
|
+
httpServer.listen(8080);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
[MIT](./LICENSE)
|
package/dist/App.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type Module } from "./Module.js";
|
|
2
|
-
import
|
|
2
|
+
import { Component } from "./Component.js";
|
|
3
3
|
export declare class App {
|
|
4
4
|
private readonly assets;
|
|
5
5
|
private readonly viewRenderer;
|
|
6
6
|
private readonly router;
|
|
7
7
|
private readonly middlewares;
|
|
8
8
|
private constructor();
|
|
9
|
-
static create(
|
|
9
|
+
static create({ components, modules, }: {
|
|
10
|
+
components: Component[];
|
|
11
|
+
modules: Module[];
|
|
12
|
+
}): Promise<App>;
|
|
10
13
|
fetch(req: Request): Promise<Response>;
|
|
11
14
|
private renderHttp404Error;
|
|
12
15
|
private renderHttp500Error;
|
package/dist/App.js
CHANGED
|
@@ -11,8 +11,9 @@ import { BaseI18nService } from "./I18nService.js";
|
|
|
11
11
|
import { BaseTemplateService } from "./TemplateService.js";
|
|
12
12
|
import { runMigrations } from "./utils/runMigrations.js";
|
|
13
13
|
import { createDebug } from "./utils/createDebug.js";
|
|
14
|
+
import { DB } from "./DB.js";
|
|
14
15
|
import { isProduction } from "./utils/isProduction.js";
|
|
15
|
-
import { ComponentFactory } from "./Component.js";
|
|
16
|
+
import { Component, ComponentFactory } from "./Component.js";
|
|
16
17
|
import { PubSubService } from "./PubSubService.js";
|
|
17
18
|
import { FS } from "./utils/fs.js";
|
|
18
19
|
import { EnvironmentBasedConfigService } from "./ConfigService.js";
|
|
@@ -57,7 +58,13 @@ export class App {
|
|
|
57
58
|
}
|
|
58
59
|
this.fetch = this.fetch.bind(this);
|
|
59
60
|
}
|
|
60
|
-
static async create(
|
|
61
|
+
static async create({ components, modules, }) {
|
|
62
|
+
const db = components.find((c) => {
|
|
63
|
+
return c instanceof DB;
|
|
64
|
+
});
|
|
65
|
+
if (!db) {
|
|
66
|
+
throw new Error("no DB component found");
|
|
67
|
+
}
|
|
61
68
|
debug("starting app in %s mode", isProduction ? "production" : "development");
|
|
62
69
|
const componentFactory = new ComponentFactory();
|
|
63
70
|
let viewRenderer;
|
|
@@ -70,11 +77,11 @@ export class App {
|
|
|
70
77
|
});
|
|
71
78
|
debug("loading modules");
|
|
72
79
|
const moduleDefinitions = await loadModules(componentFactory, modules);
|
|
73
|
-
const
|
|
80
|
+
const allComponents = componentFactory.build(new ModuleDefinitions(moduleDefinitions), ...components);
|
|
74
81
|
debug("running migrations");
|
|
75
82
|
await runMigrations(db, moduleDefinitions);
|
|
76
83
|
debug("initializing all components");
|
|
77
|
-
await Promise.all(
|
|
84
|
+
await Promise.all(allComponents.map((component) => component.init()));
|
|
78
85
|
const assets = new Map();
|
|
79
86
|
for (const module of modules) {
|
|
80
87
|
if (!module.assetsDir) {
|
|
@@ -96,6 +103,7 @@ export class App {
|
|
|
96
103
|
}
|
|
97
104
|
async fetch(req) {
|
|
98
105
|
const url = new URL(req.url, "http://localhost");
|
|
106
|
+
const locale = this.viewRenderer.computeLocale(req);
|
|
99
107
|
const ctx = {
|
|
100
108
|
startedAt: new Date(),
|
|
101
109
|
method: req.method,
|
|
@@ -104,6 +112,7 @@ export class App {
|
|
|
104
112
|
headers: req.headers,
|
|
105
113
|
cookies: parseCookieHeader(req.headers.get("cookie") || ""),
|
|
106
114
|
abortSignal: req.signal,
|
|
115
|
+
locale,
|
|
107
116
|
responseHeaders: new Headers(),
|
|
108
117
|
render: async (view, data = {}) => {
|
|
109
118
|
try {
|
package/dist/Context.d.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { type Brand } from "./utils/types.js";
|
|
2
|
+
export type InternalUserId = Brand<bigint, "InternalUserId">;
|
|
2
3
|
export type UserId = Brand<string, "UserId">;
|
|
3
|
-
export type
|
|
4
|
+
export type InternalGroupId = Brand<bigint, "InternalGroupId">;
|
|
5
|
+
export type GroupId = Brand<string, "GroupId">;
|
|
6
|
+
export type Role = Brand<number, "Role">;
|
|
4
7
|
export type AdminUserId = Brand<number, "AdminUserId">;
|
|
5
|
-
export interface
|
|
8
|
+
export interface ConnectedUser {
|
|
9
|
+
internalId: InternalUserId;
|
|
6
10
|
id: UserId;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
firstName: string;
|
|
12
|
+
lastName: string;
|
|
13
|
+
email: string;
|
|
14
|
+
groups: Array<{
|
|
15
|
+
internalId: InternalGroupId;
|
|
16
|
+
id: GroupId;
|
|
17
|
+
label: string;
|
|
18
|
+
role: Role;
|
|
10
19
|
}>;
|
|
11
20
|
}
|
|
12
21
|
export interface Admin {
|
|
@@ -22,7 +31,7 @@ export interface Span {
|
|
|
22
31
|
export interface Context {
|
|
23
32
|
startedAt: Date;
|
|
24
33
|
tx?: any;
|
|
25
|
-
user?:
|
|
34
|
+
user?: ConnectedUser;
|
|
26
35
|
admin?: Admin;
|
|
27
36
|
tracing: {
|
|
28
37
|
enabled: boolean;
|
package/dist/HttpContext.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Context } from "./Context.js";
|
|
2
2
|
import { type HttpMethod } from "./Router.js";
|
|
3
|
+
import { type Locale } from "./contrib/accept-language-parser.js";
|
|
3
4
|
export interface HttpContext<Payload = any, PathParams = any, QueryParams = any> extends Context {
|
|
4
5
|
method: HttpMethod;
|
|
5
6
|
payload: Payload;
|
|
@@ -9,6 +10,7 @@ export interface HttpContext<Payload = any, PathParams = any, QueryParams = any>
|
|
|
9
10
|
headers: Headers;
|
|
10
11
|
cookies: Record<string, string>;
|
|
11
12
|
abortSignal: AbortSignal;
|
|
13
|
+
locale: Locale;
|
|
12
14
|
responseHeaders: Headers;
|
|
13
15
|
sessionId?: string;
|
|
14
16
|
adminSessionId?: string;
|
package/dist/HttpContext.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {} from "./Context.js";
|
|
2
2
|
import {} from "./Router.js";
|
|
3
|
+
import {} from "./contrib/accept-language-parser.js";
|
|
3
4
|
export var HttpRedirectCode;
|
|
4
5
|
(function (HttpRedirectCode) {
|
|
5
6
|
HttpRedirectCode[HttpRedirectCode["HTTP_301_MOVED_PERMANENTLY"] = 301] = "HTTP_301_MOVED_PERMANENTLY";
|
package/dist/Repository.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Component, INJECT } from "./Component.js";
|
|
2
2
|
import { DB } from "./DB.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Context } from "./Context.js";
|
|
4
4
|
import { Statement } from "./utils/sql.js";
|
|
5
|
+
export declare class EntityNotFoundError extends Error {
|
|
6
|
+
}
|
|
5
7
|
export declare abstract class Repository<ID, T extends Record<string, any>> extends Component {
|
|
6
8
|
protected readonly db: DB;
|
|
7
9
|
static [INJECT]: (typeof DB)[];
|
|
@@ -10,6 +12,7 @@ export declare abstract class Repository<ID, T extends Record<string, any>> exte
|
|
|
10
12
|
protected dateFields: string[];
|
|
11
13
|
constructor(db: DB);
|
|
12
14
|
findById(ctx: Context, id: ID): Promise<T | undefined>;
|
|
15
|
+
deleteById(ctx: Context, id: ID): Promise<void>;
|
|
13
16
|
private idClause;
|
|
14
17
|
save(ctx: Context, entity: Partial<T>): Promise<T>;
|
|
15
18
|
startTransaction(ctx: Context, fn: () => Promise<void>): void | Promise<void>;
|
|
@@ -21,28 +24,6 @@ export declare abstract class Repository<ID, T extends Record<string, any>> exte
|
|
|
21
24
|
protected onBeforeInsert(ctx: Context, entity: Partial<T>): void;
|
|
22
25
|
protected onBeforeUpdate(ctx: Context, entity: Partial<T>): void;
|
|
23
26
|
}
|
|
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
27
|
export interface Page<T> {
|
|
47
28
|
items: T[];
|
|
48
29
|
}
|
package/dist/Repository.js
CHANGED
|
@@ -3,6 +3,8 @@ import { snakeToCamelCase } from "./utils/snakeToCamelCase.js";
|
|
|
3
3
|
import { Component, INJECT } from "./Component.js";
|
|
4
4
|
import { DB } from "./DB.js";
|
|
5
5
|
import { sql, Statement } from "./utils/sql.js";
|
|
6
|
+
export class EntityNotFoundError extends Error {
|
|
7
|
+
}
|
|
6
8
|
export class Repository extends Component {
|
|
7
9
|
db;
|
|
8
10
|
static [INJECT] = [DB];
|
|
@@ -16,6 +18,13 @@ export class Repository extends Component {
|
|
|
16
18
|
const query = sql.select().from(this.tableName).where(this.idClause(id));
|
|
17
19
|
return this.one(ctx, query);
|
|
18
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
|
+
}
|
|
19
28
|
idClause(id) {
|
|
20
29
|
if (Array.isArray(this.idField)) {
|
|
21
30
|
return this.idField.reduce((acc, k) => {
|
|
@@ -99,35 +108,3 @@ export class Repository extends Component {
|
|
|
99
108
|
onBeforeInsert(ctx, entity) { }
|
|
100
109
|
onBeforeUpdate(ctx, entity) { }
|
|
101
110
|
}
|
|
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/ViewRenderer.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class ViewRenderer extends Component {
|
|
|
10
10
|
private readonly modules;
|
|
11
11
|
static [INJECT]: (typeof ModuleDefinitions | typeof TemplateService | typeof BaseTemplateService | typeof I18nService)[];
|
|
12
12
|
constructor(i18nService: I18nService, templateService: BaseTemplateService, customTemplateService: TemplateService, modules: ModuleDefinitions);
|
|
13
|
+
computeLocale(req: Request): import("./contrib/accept-language-parser.js").Locale;
|
|
13
14
|
render(ctx: HttpContext, view: string | string[], data?: Record<string, any>): Promise<import("undici-types").Response>;
|
|
14
15
|
private renderTemplate;
|
|
15
16
|
}
|
package/dist/ViewRenderer.js
CHANGED
|
@@ -22,6 +22,9 @@ export class ViewRenderer extends Component {
|
|
|
22
22
|
this.customTemplateService = customTemplateService;
|
|
23
23
|
this.modules = modules;
|
|
24
24
|
}
|
|
25
|
+
computeLocale(req) {
|
|
26
|
+
return pick(this.i18nService.availableLocales(), req.headers.get("accept-language") || "");
|
|
27
|
+
}
|
|
25
28
|
async render(ctx, view, data = {}) {
|
|
26
29
|
const templates = Array.isArray(view) ? view.reverse() : [view];
|
|
27
30
|
data.TITLE = "Tymber";
|
|
@@ -29,10 +32,8 @@ export class ViewRenderer extends Component {
|
|
|
29
32
|
data.CTX.app = data.CTX.app || {};
|
|
30
33
|
data.CTX.app.isProduction = isProduction;
|
|
31
34
|
data.CTX.app.modules = this.modules.modules;
|
|
32
|
-
const locale = pick(this.i18nService.availableLocales(), ctx.headers.get("accept-language") || "");
|
|
33
|
-
data.CTX.locale = locale;
|
|
34
35
|
data.$t = (key, ...args) => {
|
|
35
|
-
return this.i18nService.translate(ctx, locale, key, ...args);
|
|
36
|
+
return this.i18nService.translate(ctx, ctx.locale, key, ...args);
|
|
36
37
|
};
|
|
37
38
|
for (const template of templates) {
|
|
38
39
|
data.VIEW = await this.renderTemplate(template, data);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
export { Component, type Ctor, ComponentFactory, INJECT } from "./Component.js";
|
|
2
|
-
export { type UserId, type
|
|
2
|
+
export { type InternalUserId, type UserId, type InternalGroupId, type GroupId, type Role, type ConnectedUser, type AdminUserId, type Admin, type Context, emptyContext, } from "./Context.js";
|
|
3
3
|
export { ConfigService } from "./ConfigService.js";
|
|
4
4
|
export { DB, DuplicateKeyError } from "./DB.js";
|
|
5
5
|
export { EventEmitter } from "./EventEmitter.js";
|
|
6
6
|
export { PubSubService, NodeClusterPubSubService, initPrimary, } from "./PubSubService.js";
|
|
7
|
+
export { I18nService } from "./I18nService.js";
|
|
7
8
|
export { Endpoint, AdminEndpoint } from "./Endpoint.js";
|
|
8
9
|
export { type HttpContext } from "./HttpContext.js";
|
|
9
10
|
export { App } from "./App.js";
|
|
10
11
|
export { type Module, type AppInit, ModuleDefinitions, type Route, } from "./Module.js";
|
|
11
12
|
export { View, AdminView } from "./View.js";
|
|
12
13
|
export { Middleware } from "./Middleware.js";
|
|
13
|
-
export { Repository, type Page,
|
|
14
|
+
export { Repository, type Page, EntityNotFoundError } from "./Repository.js";
|
|
14
15
|
export { TemplateService } from "./TemplateService.js";
|
|
15
16
|
export { createCookie, parseCookieHeader } from "./contrib/cookie.js";
|
|
16
17
|
export { AJV_INSTANCE } from "./utils/ajv.js";
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,14 @@ export { ConfigService } from "./ConfigService.js";
|
|
|
4
4
|
export { DB, DuplicateKeyError } from "./DB.js";
|
|
5
5
|
export { EventEmitter } from "./EventEmitter.js";
|
|
6
6
|
export { PubSubService, NodeClusterPubSubService, initPrimary, } from "./PubSubService.js";
|
|
7
|
+
export { I18nService } from "./I18nService.js";
|
|
7
8
|
export { Endpoint, AdminEndpoint } from "./Endpoint.js";
|
|
8
9
|
export {} from "./HttpContext.js";
|
|
9
10
|
export { App } from "./App.js";
|
|
10
11
|
export { ModuleDefinitions, } from "./Module.js";
|
|
11
12
|
export { View, AdminView } from "./View.js";
|
|
12
13
|
export { Middleware } from "./Middleware.js";
|
|
13
|
-
export { Repository,
|
|
14
|
+
export { Repository, EntityNotFoundError } from "./Repository.js";
|
|
14
15
|
export { TemplateService } from "./TemplateService.js";
|
|
15
16
|
export { createCookie, parseCookieHeader } from "./contrib/cookie.js";
|
|
16
17
|
export { AJV_INSTANCE } from "./utils/ajv.js";
|
|
@@ -22,7 +22,10 @@ export async function createTestApp(initDB, modules) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
const { httpServer, baseUrl, db } = sharedTestContext;
|
|
25
|
-
const app = await App.create(
|
|
25
|
+
const app = await App.create({
|
|
26
|
+
components: [db],
|
|
27
|
+
modules,
|
|
28
|
+
});
|
|
26
29
|
httpServer.removeAllListeners("request");
|
|
27
30
|
httpServer.on("request", toNodeHandler(app.fetch.bind(app)));
|
|
28
31
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tymber/common",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "The base of the Tymber framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Damien ARRACHEQUESNE",
|
|
@@ -29,5 +29,10 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"ajv": "~8.17.1",
|
|
31
31
|
"ajv-formats": "~3.0.1"
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"tymber",
|
|
35
|
+
"typescript",
|
|
36
|
+
"framework"
|
|
37
|
+
]
|
|
33
38
|
}
|
package/src/App.ts
CHANGED
|
@@ -15,9 +15,9 @@ import { BaseI18nService } from "./I18nService.js";
|
|
|
15
15
|
import { BaseTemplateService } from "./TemplateService.js";
|
|
16
16
|
import { runMigrations } from "./utils/runMigrations.js";
|
|
17
17
|
import { createDebug } from "./utils/createDebug.js";
|
|
18
|
-
import
|
|
18
|
+
import { DB } from "./DB.js";
|
|
19
19
|
import { isProduction } from "./utils/isProduction.js";
|
|
20
|
-
import { ComponentFactory } from "./Component.js";
|
|
20
|
+
import { Component, ComponentFactory } from "./Component.js";
|
|
21
21
|
import { PubSubService } from "./PubSubService.js";
|
|
22
22
|
import { FS } from "./utils/fs.js";
|
|
23
23
|
import { EnvironmentBasedConfigService } from "./ConfigService.js";
|
|
@@ -75,7 +75,21 @@ export class App {
|
|
|
75
75
|
this.fetch = this.fetch.bind(this);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
static async create(
|
|
78
|
+
static async create({
|
|
79
|
+
components,
|
|
80
|
+
modules,
|
|
81
|
+
}: {
|
|
82
|
+
components: Component[];
|
|
83
|
+
modules: Module[];
|
|
84
|
+
}) {
|
|
85
|
+
const db = components.find((c) => {
|
|
86
|
+
return c instanceof DB;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!db) {
|
|
90
|
+
throw new Error("no DB component found");
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
debug(
|
|
80
94
|
"starting app in %s mode",
|
|
81
95
|
isProduction ? "production" : "development",
|
|
@@ -95,16 +109,16 @@ export class App {
|
|
|
95
109
|
|
|
96
110
|
debug("loading modules");
|
|
97
111
|
const moduleDefinitions = await loadModules(componentFactory, modules);
|
|
98
|
-
const
|
|
99
|
-
db,
|
|
112
|
+
const allComponents = componentFactory.build(
|
|
100
113
|
new ModuleDefinitions(moduleDefinitions),
|
|
114
|
+
...components,
|
|
101
115
|
);
|
|
102
116
|
|
|
103
117
|
debug("running migrations");
|
|
104
118
|
await runMigrations(db, moduleDefinitions);
|
|
105
119
|
|
|
106
120
|
debug("initializing all components");
|
|
107
|
-
await Promise.all(
|
|
121
|
+
await Promise.all(allComponents.map((component) => component.init()));
|
|
108
122
|
|
|
109
123
|
const assets = new Map<string, string>();
|
|
110
124
|
|
|
@@ -140,6 +154,8 @@ export class App {
|
|
|
140
154
|
public async fetch(req: Request): Promise<Response> {
|
|
141
155
|
const url = new URL(req.url, "http://localhost");
|
|
142
156
|
|
|
157
|
+
const locale = this.viewRenderer.computeLocale(req);
|
|
158
|
+
|
|
143
159
|
const ctx = {
|
|
144
160
|
startedAt: new Date(),
|
|
145
161
|
method: req.method as HttpMethod,
|
|
@@ -148,6 +164,7 @@ export class App {
|
|
|
148
164
|
headers: req.headers,
|
|
149
165
|
cookies: parseCookieHeader(req.headers.get("cookie") || ""),
|
|
150
166
|
abortSignal: req.signal,
|
|
167
|
+
locale,
|
|
151
168
|
responseHeaders: new Headers(),
|
|
152
169
|
|
|
153
170
|
render: async (view, data = {}) => {
|
package/src/Context.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { type Brand } from "./utils/types.js";
|
|
2
2
|
|
|
3
|
+
export type InternalUserId = Brand<bigint, "InternalUserId">;
|
|
3
4
|
export type UserId = Brand<string, "UserId">;
|
|
4
|
-
export type
|
|
5
|
+
export type InternalGroupId = Brand<bigint, "InternalGroupId">;
|
|
6
|
+
export type GroupId = Brand<string, "GroupId">;
|
|
7
|
+
export type Role = Brand<number, "Role">;
|
|
5
8
|
export type AdminUserId = Brand<number, "AdminUserId">;
|
|
6
9
|
|
|
7
|
-
export interface
|
|
10
|
+
export interface ConnectedUser {
|
|
11
|
+
internalId: InternalUserId;
|
|
8
12
|
id: UserId;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
firstName: string;
|
|
15
|
+
lastName: string;
|
|
16
|
+
email: string;
|
|
17
|
+
|
|
18
|
+
groups: Array<{
|
|
19
|
+
internalId: InternalGroupId;
|
|
20
|
+
id: GroupId;
|
|
21
|
+
label: string;
|
|
22
|
+
role: Role;
|
|
12
23
|
}>;
|
|
13
24
|
}
|
|
14
25
|
|
|
@@ -29,7 +40,7 @@ export interface Context {
|
|
|
29
40
|
|
|
30
41
|
tx?: any;
|
|
31
42
|
|
|
32
|
-
user?:
|
|
43
|
+
user?: ConnectedUser;
|
|
33
44
|
admin?: Admin;
|
|
34
45
|
|
|
35
46
|
tracing: {
|
package/src/HttpContext.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Context } from "./Context.js";
|
|
2
2
|
import { type HttpMethod } from "./Router.js";
|
|
3
|
+
import { type Locale } from "./contrib/accept-language-parser.js";
|
|
3
4
|
|
|
4
5
|
export interface HttpContext<Payload = any, PathParams = any, QueryParams = any>
|
|
5
6
|
extends Context {
|
|
@@ -12,6 +13,7 @@ export interface HttpContext<Payload = any, PathParams = any, QueryParams = any>
|
|
|
12
13
|
cookies: Record<string, string>;
|
|
13
14
|
abortSignal: AbortSignal;
|
|
14
15
|
|
|
16
|
+
locale: Locale;
|
|
15
17
|
responseHeaders: Headers;
|
|
16
18
|
sessionId?: string;
|
|
17
19
|
adminSessionId?: string;
|
package/src/Repository.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { camelToSnakeCase } from "./utils/camelToSnakeCase.js";
|
|
|
2
2
|
import { snakeToCamelCase } from "./utils/snakeToCamelCase.js";
|
|
3
3
|
import { Component, INJECT } from "./Component.js";
|
|
4
4
|
import { DB } from "./DB.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Context } from "./Context.js";
|
|
6
6
|
import { sql, Statement } from "./utils/sql.js";
|
|
7
7
|
|
|
8
|
+
export class EntityNotFoundError extends Error {}
|
|
9
|
+
|
|
8
10
|
export abstract class Repository<
|
|
9
11
|
ID,
|
|
10
12
|
T extends Record<string, any>,
|
|
@@ -25,6 +27,16 @@ export abstract class Repository<
|
|
|
25
27
|
return this.one(ctx, query);
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
async deleteById(ctx: Context, id: ID) {
|
|
31
|
+
const query = sql.deleteFrom(this.tableName).where(this.idClause(id));
|
|
32
|
+
|
|
33
|
+
const res = await this.db.run(ctx, query);
|
|
34
|
+
|
|
35
|
+
if (res.affectedRows !== 1) {
|
|
36
|
+
throw new EntityNotFoundError();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
private idClause(id: ID) {
|
|
29
41
|
if (Array.isArray(this.idField)) {
|
|
30
42
|
return this.idField.reduce((acc, k) => {
|
|
@@ -141,64 +153,6 @@ export abstract class Repository<
|
|
|
141
153
|
protected onBeforeUpdate(ctx: Context, entity: Partial<T>) {}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
export interface AuditedEntity {
|
|
145
|
-
createdBy?: UserId;
|
|
146
|
-
createdAt?: Date;
|
|
147
|
-
updatedBy?: UserId;
|
|
148
|
-
updatedAt?: Date;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export abstract class AuditedRepository<
|
|
152
|
-
ID,
|
|
153
|
-
T extends AuditedEntity,
|
|
154
|
-
> extends Repository<ID, T> {
|
|
155
|
-
protected override dateFields = ["createdAt", "updatedAt"];
|
|
156
|
-
|
|
157
|
-
protected override onBeforeInsert(ctx: Context, entity: Partial<T>) {
|
|
158
|
-
entity.createdAt = ctx.startedAt;
|
|
159
|
-
entity.updatedAt = ctx.startedAt;
|
|
160
|
-
if (ctx.user) {
|
|
161
|
-
entity.createdBy = ctx.user.id;
|
|
162
|
-
entity.updatedBy = ctx.user.id;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
protected override onBeforeUpdate(ctx: Context, entity: Partial<T>) {
|
|
167
|
-
entity.updatedAt = ctx.startedAt;
|
|
168
|
-
if (ctx.user) {
|
|
169
|
-
entity.updatedBy = ctx.user.id;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface AdminAuditedEntity {
|
|
175
|
-
createdBy?: AdminUserId;
|
|
176
|
-
createdAt?: Date;
|
|
177
|
-
updatedBy?: AdminUserId;
|
|
178
|
-
updatedAt?: Date;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export abstract class AdminAuditedRepository<
|
|
182
|
-
ID,
|
|
183
|
-
T extends AdminAuditedEntity,
|
|
184
|
-
> extends Repository<ID, T> {
|
|
185
|
-
protected override dateFields = ["createdAt", "updatedAt"];
|
|
186
|
-
|
|
187
|
-
protected override onBeforeInsert(ctx: Context, entity: Partial<T>) {
|
|
188
|
-
entity.createdAt = ctx.startedAt;
|
|
189
|
-
if (ctx.admin) {
|
|
190
|
-
entity.createdBy = ctx.admin.id;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
protected override onBeforeUpdate(ctx: Context, entity: Partial<T>) {
|
|
195
|
-
entity.updatedAt = ctx.startedAt;
|
|
196
|
-
if (ctx.admin) {
|
|
197
|
-
entity.updatedBy = ctx.admin.id;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
156
|
export interface Page<T> {
|
|
203
157
|
items: T[];
|
|
204
158
|
}
|
package/src/ViewRenderer.ts
CHANGED
|
@@ -23,6 +23,13 @@ export class ViewRenderer extends Component {
|
|
|
23
23
|
super();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
public computeLocale(req: Request) {
|
|
27
|
+
return pick(
|
|
28
|
+
this.i18nService.availableLocales(),
|
|
29
|
+
req.headers.get("accept-language") || "",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
public async render(
|
|
27
34
|
ctx: HttpContext,
|
|
28
35
|
view: string | string[],
|
|
@@ -36,15 +43,8 @@ export class ViewRenderer extends Component {
|
|
|
36
43
|
data.CTX.app.isProduction = isProduction;
|
|
37
44
|
data.CTX.app.modules = this.modules.modules;
|
|
38
45
|
|
|
39
|
-
const locale = pick(
|
|
40
|
-
this.i18nService.availableLocales(),
|
|
41
|
-
ctx.headers.get("accept-language") || "",
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
data.CTX.locale = locale;
|
|
45
|
-
|
|
46
46
|
data.$t = (key: string, ...args: any[]) => {
|
|
47
|
-
return this.i18nService.translate(ctx, locale, key, ...args);
|
|
47
|
+
return this.i18nService.translate(ctx, ctx.locale, key, ...args);
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
for (const template of templates) {
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { Component, type Ctor, ComponentFactory, INJECT } from "./Component.js";
|
|
2
2
|
export {
|
|
3
|
+
type InternalUserId,
|
|
3
4
|
type UserId,
|
|
4
|
-
type
|
|
5
|
-
type
|
|
5
|
+
type InternalGroupId,
|
|
6
|
+
type GroupId,
|
|
7
|
+
type Role,
|
|
8
|
+
type ConnectedUser,
|
|
6
9
|
type AdminUserId,
|
|
7
10
|
type Admin,
|
|
8
11
|
type Context,
|
|
@@ -16,6 +19,7 @@ export {
|
|
|
16
19
|
NodeClusterPubSubService,
|
|
17
20
|
initPrimary,
|
|
18
21
|
} from "./PubSubService.js";
|
|
22
|
+
export { I18nService } from "./I18nService.js";
|
|
19
23
|
export { Endpoint, AdminEndpoint } from "./Endpoint.js";
|
|
20
24
|
export { type HttpContext } from "./HttpContext.js";
|
|
21
25
|
export { App } from "./App.js";
|
|
@@ -27,14 +31,7 @@ export {
|
|
|
27
31
|
} from "./Module.js";
|
|
28
32
|
export { View, AdminView } from "./View.js";
|
|
29
33
|
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";
|
|
34
|
+
export { Repository, type Page, EntityNotFoundError } from "./Repository.js";
|
|
38
35
|
export { TemplateService } from "./TemplateService.js";
|
|
39
36
|
|
|
40
37
|
export { createCookie, parseCookieHeader } from "./contrib/cookie.js";
|
|
@@ -43,7 +43,10 @@ export async function createTestApp(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const { httpServer, baseUrl, db } = sharedTestContext;
|
|
46
|
-
const app = await App.create(
|
|
46
|
+
const app = await App.create({
|
|
47
|
+
components: [db],
|
|
48
|
+
modules,
|
|
49
|
+
});
|
|
47
50
|
|
|
48
51
|
httpServer.removeAllListeners("request");
|
|
49
52
|
httpServer.on("request", toNodeHandler(app.fetch.bind(app)));
|