@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 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 type { DB } from "./DB.js";
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(db: DB, modules: Module[]): Promise<App>;
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(db, modules) {
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 components = componentFactory.build(db, new ModuleDefinitions(moduleDefinitions));
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(components.map((component) => component.init()));
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 OrgId = Brand<string, "OrgId">;
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 User {
8
+ export interface ConnectedUser {
9
+ internalId: InternalUserId;
6
10
  id: UserId;
7
- orgs: Array<{
8
- id: OrgId;
9
- role: string;
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?: User;
34
+ user?: ConnectedUser;
26
35
  admin?: Admin;
27
36
  tracing: {
28
37
  enabled: boolean;
@@ -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;
@@ -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";
@@ -1,7 +1,9 @@
1
1
  import { Component, INJECT } from "./Component.js";
2
2
  import { DB } from "./DB.js";
3
- import type { AdminUserId, Context, UserId } from "./Context.js";
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
  }
@@ -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
- }
@@ -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
  }
@@ -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 User, type OrgId, type AdminUserId, type Admin, type Context, emptyContext, } from "./Context.js";
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, type AuditedEntity, AuditedRepository, type AdminAuditedEntity, AdminAuditedRepository, } from "./Repository.js";
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, AuditedRepository, AdminAuditedRepository, } from "./Repository.js";
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(db, modules);
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.1",
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 type { DB } from "./DB.js";
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(db: DB, modules: Module[]) {
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 components = componentFactory.build(
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(components.map((component) => component.init()));
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 OrgId = Brand<string, "OrgId">;
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 User {
10
+ export interface ConnectedUser {
11
+ internalId: InternalUserId;
8
12
  id: UserId;
9
- orgs: Array<{
10
- id: OrgId;
11
- role: string;
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?: User;
43
+ user?: ConnectedUser;
33
44
  admin?: Admin;
34
45
 
35
46
  tracing: {
@@ -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 { AdminUserId, Context, UserId } from "./Context.js";
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
  }
@@ -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 User,
5
- type OrgId,
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(db, modules);
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)));