@getsoda/core 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +58 -0
  2. package/dist/app/app.d.ts +17 -1
  3. package/dist/app/app.d.ts.map +1 -1
  4. package/dist/app/app.js +46 -10
  5. package/dist/app/app.js.map +1 -1
  6. package/dist/di/container.d.ts +7 -0
  7. package/dist/di/container.d.ts.map +1 -1
  8. package/dist/di/container.js +24 -1
  9. package/dist/di/container.js.map +1 -1
  10. package/dist/di/graph.d.ts +2 -2
  11. package/dist/di/graph.d.ts.map +1 -1
  12. package/dist/di/graph.js +20 -10
  13. package/dist/di/graph.js.map +1 -1
  14. package/dist/http/arguments.d.ts +20 -2
  15. package/dist/http/arguments.d.ts.map +1 -1
  16. package/dist/http/arguments.js +29 -0
  17. package/dist/http/arguments.js.map +1 -1
  18. package/dist/http/body.d.ts +28 -0
  19. package/dist/http/body.d.ts.map +1 -1
  20. package/dist/http/body.js +96 -1
  21. package/dist/http/body.js.map +1 -1
  22. package/dist/http/context-key.d.ts +33 -0
  23. package/dist/http/context-key.d.ts.map +1 -0
  24. package/dist/http/context-key.js +37 -0
  25. package/dist/http/context-key.js.map +1 -0
  26. package/dist/http/context.d.ts +37 -3
  27. package/dist/http/context.d.ts.map +1 -1
  28. package/dist/http/context.js +63 -5
  29. package/dist/http/context.js.map +1 -1
  30. package/dist/http/cookies.d.ts +49 -0
  31. package/dist/http/cookies.d.ts.map +1 -0
  32. package/dist/http/cookies.js +77 -0
  33. package/dist/http/cookies.js.map +1 -0
  34. package/dist/http/cors.d.ts +41 -0
  35. package/dist/http/cors.d.ts.map +1 -0
  36. package/dist/http/cors.js +97 -0
  37. package/dist/http/cors.js.map +1 -0
  38. package/dist/http/index.d.ts +8 -2
  39. package/dist/http/index.d.ts.map +1 -1
  40. package/dist/http/index.js +4 -1
  41. package/dist/http/index.js.map +1 -1
  42. package/dist/http/middleware.d.ts +56 -0
  43. package/dist/http/middleware.d.ts.map +1 -0
  44. package/dist/http/middleware.js +83 -0
  45. package/dist/http/middleware.js.map +1 -0
  46. package/dist/http/parsers.d.ts +3 -0
  47. package/dist/http/parsers.d.ts.map +1 -1
  48. package/dist/http/parsers.js +2 -1
  49. package/dist/http/parsers.js.map +1 -1
  50. package/dist/http/routes.d.ts +10 -1
  51. package/dist/http/routes.d.ts.map +1 -1
  52. package/dist/http/routes.js +4 -1
  53. package/dist/http/routes.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/jwt/algorithms.d.ts +19 -0
  59. package/dist/jwt/algorithms.d.ts.map +1 -0
  60. package/dist/jwt/algorithms.js +60 -0
  61. package/dist/jwt/algorithms.js.map +1 -0
  62. package/dist/jwt/errors.d.ts +24 -0
  63. package/dist/jwt/errors.d.ts.map +1 -0
  64. package/dist/jwt/errors.js +14 -0
  65. package/dist/jwt/errors.js.map +1 -0
  66. package/dist/jwt/guard.d.ts +30 -0
  67. package/dist/jwt/guard.d.ts.map +1 -0
  68. package/dist/jwt/guard.js +61 -0
  69. package/dist/jwt/guard.js.map +1 -0
  70. package/dist/jwt/index.d.ts +14 -0
  71. package/dist/jwt/index.d.ts.map +1 -0
  72. package/dist/jwt/index.js +10 -0
  73. package/dist/jwt/index.js.map +1 -0
  74. package/dist/jwt/module.d.ts +36 -0
  75. package/dist/jwt/module.d.ts.map +1 -0
  76. package/dist/jwt/module.js +38 -0
  77. package/dist/jwt/module.js.map +1 -0
  78. package/dist/jwt/service.d.ts +66 -0
  79. package/dist/jwt/service.d.ts.map +1 -0
  80. package/dist/jwt/service.js +138 -0
  81. package/dist/jwt/service.js.map +1 -0
  82. package/dist/logger/console-logger.d.ts +36 -0
  83. package/dist/logger/console-logger.d.ts.map +1 -0
  84. package/dist/logger/console-logger.js +117 -0
  85. package/dist/logger/console-logger.js.map +1 -0
  86. package/dist/logger/index.d.ts +9 -0
  87. package/dist/logger/index.d.ts.map +1 -0
  88. package/dist/logger/index.js +7 -0
  89. package/dist/logger/index.js.map +1 -0
  90. package/dist/logger/logger.d.ts +55 -0
  91. package/dist/logger/logger.d.ts.map +1 -0
  92. package/dist/logger/logger.js +66 -0
  93. package/dist/logger/logger.js.map +1 -0
  94. package/dist/routing/types.d.ts +1 -1
  95. package/dist/routing/types.d.ts.map +1 -1
  96. package/package.json +6 -12
  97. package/src/app/app.ts +77 -13
  98. package/src/di/container.ts +38 -2
  99. package/src/di/graph.ts +26 -11
  100. package/src/http/arguments.ts +42 -2
  101. package/src/http/body.ts +142 -1
  102. package/src/http/context-key.ts +35 -0
  103. package/src/http/context.ts +94 -8
  104. package/src/http/cookies.ts +96 -0
  105. package/src/http/cors.ts +143 -0
  106. package/src/http/index.ts +8 -2
  107. package/src/http/middleware.ts +124 -0
  108. package/src/http/parsers.ts +2 -1
  109. package/src/http/routes.ts +17 -2
  110. package/src/index.ts +1 -0
  111. package/src/jwt/algorithms.ts +113 -0
  112. package/src/jwt/errors.ts +30 -0
  113. package/src/jwt/guard.ts +74 -0
  114. package/src/jwt/index.ts +14 -0
  115. package/src/jwt/module.ts +58 -0
  116. package/src/jwt/service.ts +216 -0
  117. package/src/logger/console-logger.ts +163 -0
  118. package/src/logger/index.ts +9 -0
  119. package/src/logger/logger.ts +96 -0
  120. package/src/routing/types.ts +1 -1
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @module
3
+ * Logger writing to stdout and stderr, readable for people or as JSON lines.
4
+ *
5
+ * ```
6
+ * [Soda] 41250 - 11/14/2026, 12:22:09 AM LOG [UsersService] User created {"id":7} +3ms
7
+ * {"time":"2026-11-13T21:22:09.120Z","level":"log","context":"UsersService","message":"User created","id":7}
8
+ * ```
9
+ *
10
+ * Like Nest's `ConsoleLogger`: the same line layout, colored by level in a
11
+ * terminal, with the time since the previous line; a minimum level; a JSON
12
+ * mode for log collectors. `error` and `fatal` go to stderr, the rest to stdout.
13
+ */
14
+ import { Logger } from './logger.js';
15
+ const SEVERITY = { verbose: 0, debug: 1, log: 2, warn: 3, error: 4, fatal: 5 };
16
+ const color = (code) => (text) => `\u001B[${code}m${text}\u001B[0m`;
17
+ const LEVEL_COLORS = {
18
+ verbose: color('96'),
19
+ debug: color('95'),
20
+ log: color('32'),
21
+ warn: color('33'),
22
+ error: color('31'),
23
+ fatal: color('1;31'),
24
+ };
25
+ const yellow = color('38;5;3');
26
+ const plain = (text) => text;
27
+ const dateTime = new Intl.DateTimeFormat(undefined, {
28
+ year: 'numeric',
29
+ month: '2-digit',
30
+ day: '2-digit',
31
+ hour: 'numeric',
32
+ minute: '2-digit',
33
+ second: '2-digit',
34
+ });
35
+ const writeToConsole = (line, level) => {
36
+ const stream = level === 'error' || level === 'fatal' ? process.stderr : process.stdout;
37
+ stream.write(`${line}\n`);
38
+ };
39
+ const serializeError = (error) => {
40
+ const serialized = { name: error.name, message: error.message, stack: error.stack };
41
+ if (error.cause !== undefined) {
42
+ serialized.cause = error.cause instanceof Error ? serializeError(error.cause) : error.cause;
43
+ }
44
+ return serialized;
45
+ };
46
+ /**
47
+ * `JSON.stringify` that never throws: bigints become strings, an object met a
48
+ * second time (a cycle) becomes `"[Circular]"`. Errors are serialized in full
49
+ * (stack and cause) for JSON lines, or as `Name: message` when their stack is
50
+ * printed on its own lines.
51
+ */
52
+ const stringify = (value, errors) => {
53
+ const seen = new WeakSet();
54
+ return JSON.stringify(value, (_key, current) => {
55
+ if (typeof current === 'bigint') {
56
+ return current.toString();
57
+ }
58
+ if (current instanceof Error) {
59
+ return errors === 'full' ? serializeError(current) : `${current.name}: ${current.message}`;
60
+ }
61
+ if (typeof current === 'object' && current !== null) {
62
+ if (seen.has(current)) {
63
+ return '[Circular]';
64
+ }
65
+ seen.add(current);
66
+ }
67
+ return current;
68
+ });
69
+ };
70
+ export class ConsoleLogger extends Logger {
71
+ #minimum;
72
+ #json;
73
+ #output;
74
+ #colors;
75
+ #prefix;
76
+ /** When the previous line was written, for the `+3ms` at the end of the next. */
77
+ #last;
78
+ constructor(options = {}) {
79
+ super();
80
+ this.#minimum = SEVERITY[options.level ?? 'log'];
81
+ this.#json = options.json === true;
82
+ this.#output = options.output ?? writeToConsole;
83
+ this.#colors = options.colors ?? (options.output === undefined && process.stdout.isTTY === true && process.env.NO_COLOR === undefined);
84
+ this.#prefix = options.prefix ?? 'Soda';
85
+ }
86
+ write(entry) {
87
+ if (SEVERITY[entry.level] < this.#minimum) {
88
+ return;
89
+ }
90
+ const now = Date.now();
91
+ this.#output(this.#json ? this.#formatJson(entry, now) : this.#formatText(entry, now), entry.level);
92
+ this.#last = now;
93
+ }
94
+ #formatJson(entry, now) {
95
+ const time = new Date(now).toISOString();
96
+ // Fixed fields last: data cannot overwrite them.
97
+ return stringify({ ...entry.data, time, level: entry.level, context: entry.context, message: entry.message }, 'full');
98
+ }
99
+ #formatText(entry, now) {
100
+ const level = this.#colors ? LEVEL_COLORS[entry.level] : plain;
101
+ const accent = this.#colors ? yellow : plain;
102
+ const context = entry.context === undefined ? '' : accent(`[${entry.context}] `);
103
+ const since = this.#last === undefined ? '' : accent(` +${now - this.#last}ms`);
104
+ const data = entry.data === undefined ? '' : ` ${stringify(entry.data, 'brief')}`;
105
+ const line = `${level(`[${this.#prefix}] ${process.pid} - `)}${dateTime.format(now)} ${level(entry.level.toUpperCase().padStart(7))} ` +
106
+ `${context}${level(entry.message)}${data}${since}`;
107
+ let stacks = '';
108
+ // Stacks are read better on their own lines than inside the JSON.
109
+ for (const value of Object.values(entry.data ?? {})) {
110
+ if (value instanceof Error && value.stack !== undefined) {
111
+ stacks += `\n${value.stack}`;
112
+ }
113
+ }
114
+ return line + stacks;
115
+ }
116
+ }
117
+ //# sourceMappingURL=console-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-logger.js","sourceRoot":"","sources":["../../src/logger/console-logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module
3
+ * Logging: the `Logger` contract and token, and the console implementation.
4
+ */
5
+ export { ConsoleLogger } from './console-logger.ts';
6
+ export type { ConsoleLoggerOptions } from './console-logger.ts';
7
+ export { LOG_LEVELS, Logger, SilentLogger } from './logger.ts';
8
+ export type { LogData, LogEntry, LogLevel } from './logger.ts';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC9D,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module
3
+ * Logging: the `Logger` contract and token, and the console implementation.
4
+ */
5
+ export { ConsoleLogger } from './console-logger.js';
6
+ export { LOG_LEVELS, Logger, SilentLogger } from './logger.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module
3
+ * The logger contract.
4
+ *
5
+ * `Logger` is both the type and the DI token: the application provides one
6
+ * (a `ConsoleLogger` unless told otherwise) and any class injects it.
7
+ *
8
+ * ```ts
9
+ * class UsersService {
10
+ * readonly #logger = inject(Logger).child('UsersService')
11
+ *
12
+ * create(user: User) {
13
+ * this.#logger.log('User created', { id: user.id })
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Levels are Nest's. Unlike Nest's `Logger`, it is not a static global: each
19
+ * application has its own, and tests replace it with `overrides`.
20
+ */
21
+ /** Levels, from the most to the least verbose. */
22
+ export declare const LOG_LEVELS: readonly ['verbose', 'debug', 'log', 'warn', 'error', 'fatal'];
23
+ export type LogLevel = (typeof LOG_LEVELS)[number];
24
+ /** Structured data of an entry; `Error` values are serialized with their stack and cause. */
25
+ export type LogData = Readonly<Record<string, unknown>>;
26
+ /**
27
+ * One log entry, as a `Logger` implementation receives it.
28
+ */
29
+ export interface LogEntry {
30
+ readonly level: LogLevel;
31
+ readonly message: string;
32
+ readonly data: LogData | undefined;
33
+ /** Where it comes from, as given to `child()`. */
34
+ readonly context: string | undefined;
35
+ }
36
+ /**
37
+ * A logger. Implementations only write entries; levels and contexts are handled here.
38
+ */
39
+ export declare abstract class Logger {
40
+ /** Writes one entry. Called for every level: filtering is up to the implementation. */
41
+ abstract write(entry: LogEntry): void;
42
+ verbose(message: string, data?: LogData): void;
43
+ debug(message: string, data?: LogData): void;
44
+ log(message: string, data?: LogData): void;
45
+ warn(message: string, data?: LogData): void;
46
+ error(message: string, data?: LogData): void;
47
+ fatal(message: string, data?: LogData): void;
48
+ /** A logger writing through this one, with `context` on every entry. */
49
+ child(context: string): Logger;
50
+ }
51
+ /** A logger that writes nothing: `createApp(AppModule, { logger: false })`. */
52
+ export declare class SilentLogger extends Logger {
53
+ write(): void;
54
+ }
55
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kDAAkD;AAClD,eAAO,MAAM,UAAU,YAAI,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAU,CAAA;AAExF,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAElD,6FAA6F;AAC7F,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEvD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;CACrC;AAED;;GAEG;AACH,8BAAsB,MAAM;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAA;IAErC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE7C;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEzC;IAED,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE1C;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED,wEAAwE;IACxE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7B;CACF;AAiBD,+EAA+E;AAC/E,qBAAa,YAAa,SAAQ,MAAM;IACtC,KAAK,IAAI,IAAI,CAAG;CACjB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @module
3
+ * The logger contract.
4
+ *
5
+ * `Logger` is both the type and the DI token: the application provides one
6
+ * (a `ConsoleLogger` unless told otherwise) and any class injects it.
7
+ *
8
+ * ```ts
9
+ * class UsersService {
10
+ * readonly #logger = inject(Logger).child('UsersService')
11
+ *
12
+ * create(user: User) {
13
+ * this.#logger.log('User created', { id: user.id })
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Levels are Nest's. Unlike Nest's `Logger`, it is not a static global: each
19
+ * application has its own, and tests replace it with `overrides`.
20
+ */
21
+ /** Levels, from the most to the least verbose. */
22
+ export const LOG_LEVELS = ['verbose', 'debug', 'log', 'warn', 'error', 'fatal'];
23
+ /**
24
+ * A logger. Implementations only write entries; levels and contexts are handled here.
25
+ */
26
+ export class Logger {
27
+ verbose(message, data) {
28
+ this.write({ level: 'verbose', message, data, context: undefined });
29
+ }
30
+ debug(message, data) {
31
+ this.write({ level: 'debug', message, data, context: undefined });
32
+ }
33
+ log(message, data) {
34
+ this.write({ level: 'log', message, data, context: undefined });
35
+ }
36
+ warn(message, data) {
37
+ this.write({ level: 'warn', message, data, context: undefined });
38
+ }
39
+ error(message, data) {
40
+ this.write({ level: 'error', message, data, context: undefined });
41
+ }
42
+ fatal(message, data) {
43
+ this.write({ level: 'fatal', message, data, context: undefined });
44
+ }
45
+ /** A logger writing through this one, with `context` on every entry. */
46
+ child(context) {
47
+ return new ContextLogger(this, context);
48
+ }
49
+ }
50
+ class ContextLogger extends Logger {
51
+ #parent;
52
+ #context;
53
+ constructor(parent, context) {
54
+ super();
55
+ this.#parent = parent;
56
+ this.#context = context;
57
+ }
58
+ write(entry) {
59
+ this.#parent.write({ ...entry, context: this.#context });
60
+ }
61
+ }
62
+ /** A logger that writes nothing: `createApp(AppModule, { logger: false })`. */
63
+ export class SilentLogger extends Logger {
64
+ write() { }
65
+ }
66
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kDAAkD;AAClD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAU,CAAA;AAkBxF;;GAEG;AACH,MAAM,OAAgB,MAAM;IAI1B,OAAO,CAAC,OAAe,EAAE,IAAc;QACrC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACrE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,GAAG,CAAC,OAAe,EAAE,IAAc;QACjC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACjE,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,IAAc;QAClC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,wEAAwE;IACxE,KAAK,CAAC,OAAe;QACnB,OAAO,IAAI,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;CACF;AAED,MAAM,aAAc,SAAQ,MAAM;IACvB,OAAO,CAAQ;IACf,QAAQ,CAAQ;IAEzB,YAAY,MAAc,EAAE,OAAe;QACzC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,KAAe;QACnB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC1D,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,OAAO,YAAa,SAAQ,MAAM;IACtC,KAAK,KAAU,CAAC;CACjB"}
@@ -30,7 +30,7 @@ export type Params = Record<string, string>;
30
30
  *
31
31
  * `params` may be the very object Bun put on `req.params`: treat it as read-only.
32
32
  */
33
- export type Handler = (req: Request, params: Params) => Response | Promise<Response>;
33
+ export type Handler = (req: Bun.BunRequest, params: Params) => Response | Promise<Response>;
34
34
  /**
35
35
  * Route definition.
36
36
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,SAAU,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEpF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,oFAAoF;IACpF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjF;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC9C,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,SAAU,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3F;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,oFAAoF;IACpF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjF;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC9C,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getsoda/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Class-based web framework for Bun: modules, dependency injection and decorators like Nest, on Bun's native router",
5
5
  "keywords": [
6
6
  "bun",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "default": "./dist/index.js"
30
30
  },
31
+ "./jwt": {
32
+ "types": "./dist/jwt/index.d.ts",
33
+ "default": "./dist/jwt/index.js"
34
+ },
31
35
  "./package.json": "./package.json"
32
36
  },
33
37
  "types": "./dist/index.d.ts",
@@ -42,16 +46,6 @@
42
46
  },
43
47
  "scripts": {
44
48
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
45
- "typecheck": "bun check",
46
- "lint": "oxlint",
47
- "lint:fix": "oxlint --fix",
48
- "test": "bun test",
49
- "prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build"
50
- },
51
- "devDependencies": {
52
- "@stylistic/eslint-plugin": "5.10.0",
53
- "@types/bun": "latest",
54
- "oxlint": "1.93.0",
55
- "typescript": "7.0.2"
49
+ "prepublishOnly": "cd ../.. && bun run lint && bun run typecheck && bun run test && cd packages/core && bun run build"
56
50
  }
57
51
  }
package/src/app/app.ts CHANGED
@@ -19,11 +19,14 @@
19
19
  * are being destroyed; here they cannot.
20
20
  */
21
21
 
22
- import { createContainer } from '../di/index.ts'
22
+ import { createContainer, provide } from '../di/index.ts'
23
23
  import type { Container, ContainerOptions, ModuleImport } from '../di/index.ts'
24
24
  import { destroyAll, initializeAll } from '../di/lifecycle.ts'
25
25
  import { buildRoutes } from '../http/index.ts'
26
- import type { ControllerRoutesOptions } from '../http/index.ts'
26
+ import type { ControllerRoutesOptions, MappedController, Middleware } from '../http/index.ts'
27
+ import { errorResponse } from '../http/exceptions.ts'
28
+ import { applyMiddleware, resolveMiddleware } from '../http/middleware.ts'
29
+ import { ConsoleLogger, Logger, SilentLogger } from '../logger/index.ts'
27
30
  import { compileRoutes } from '../routing/index.ts'
28
31
  import type { CompiledRoutes, CompileOptions } from '../routing/index.ts'
29
32
  import { release, takeOver } from './hot.ts'
@@ -31,7 +34,18 @@ import { release, takeOver } from './hot.ts'
31
34
  /**
32
35
  * Options of `createApp`.
33
36
  */
34
- export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, CompileOptions {}
37
+ export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, CompileOptions {
38
+ /**
39
+ * Middleware run around every request, before routing (404 and preflights
40
+ * included), first one outermost: `[cors(), requestId]`.
41
+ */
42
+ readonly middleware?: readonly Middleware[]
43
+ /**
44
+ * Logger of the application, injectable everywhere as `inject(Logger)`.
45
+ * Defaults to a `ConsoleLogger`; `false` writes nothing.
46
+ */
47
+ readonly logger?: Logger | false
48
+ }
35
49
 
36
50
  /**
37
51
  * Where to listen. `port: 0` picks a free port.
@@ -39,6 +53,11 @@ export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, C
39
53
  export interface ListenOptions {
40
54
  readonly port?: number
41
55
  readonly hostname?: string
56
+ /**
57
+ * Largest request body accepted, in bytes; bigger ones are refused before any
58
+ * handler runs. Bun's default is 128 MiB.
59
+ */
60
+ readonly maxRequestBodySize?: number
42
61
  }
43
62
 
44
63
  /** Signals `enableShutdownSignals()` listens to by default. */
@@ -80,11 +99,13 @@ class Application implements App {
80
99
  #closing: Promise<void> | undefined
81
100
  readonly #enhancers: readonly unknown[]
82
101
  readonly #signals = new Map<NodeJS.Signals, () => void>()
102
+ readonly #logger: Logger
83
103
 
84
- constructor(container: Container, routes: CompiledRoutes, enhancers: readonly unknown[]) {
104
+ constructor(container: Container, routes: CompiledRoutes, enhancers: readonly unknown[], logger: Logger) {
85
105
  this.container = container
86
106
  this.routes = routes
87
107
  this.#enhancers = enhancers
108
+ this.#logger = logger
88
109
  }
89
110
 
90
111
  get server(): Bun.Server<undefined> | undefined {
@@ -100,9 +121,9 @@ class Application implements App {
100
121
  throw new Error(`The application already listens on ${this.#server.url.href}`)
101
122
  }
102
123
 
103
- const { port = 0, hostname } = typeof options === 'number' ? { port: options } : options
124
+ const { port = 0, hostname, maxRequestBodySize } = typeof options === 'number' ? { port: options } : options
104
125
 
105
- const server = Bun.serve({ port, hostname, ...this.routes })
126
+ const server = Bun.serve({ port, hostname, maxRequestBodySize, ...this.routes })
106
127
 
107
128
  this.#server = server
108
129
 
@@ -110,6 +131,8 @@ class Application implements App {
110
131
  await takeOver(server.port, this)
111
132
  }
112
133
 
134
+ this.#logger.log(`Listening on ${server.url.href}`)
135
+
113
136
  return server
114
137
  }
115
138
 
@@ -151,7 +174,7 @@ class Application implements App {
151
174
  process.kill(process.pid, signal)
152
175
  },
153
176
  (error: unknown) => {
154
- console.error('Error during shutdown', error)
177
+ this.#logger.fatal('Shutdown failed', { error })
155
178
  process.exit(1)
156
179
  }
157
180
  )
@@ -222,6 +245,24 @@ const destroyEverything = async (enhancers: readonly unknown[], container: Conta
222
245
  }
223
246
  }
224
247
 
248
+ /** What Nest logs at startup: the modules, then each controller and its routes. */
249
+ const logStartup = (logger: Logger, container: Container, controllers: readonly MappedController[]): void => {
250
+ const modules = logger.child('Container')
251
+ const routes = logger.child('Routes')
252
+
253
+ for (const module of container.modules) {
254
+ modules.log(`${module.name} dependencies initialized`)
255
+ }
256
+
257
+ for (const { controller, path, routes: mapped } of controllers) {
258
+ routes.log(`${controller.name} {${path}}:`)
259
+
260
+ for (const route of mapped) {
261
+ routes.log(`Mapped {${route.path}, ${route.method}} route`)
262
+ }
263
+ }
264
+ }
265
+
225
266
  /**
226
267
  * Creates the container and the routes, and runs every `onInit` hook.
227
268
  *
@@ -230,16 +271,39 @@ const destroyEverything = async (enhancers: readonly unknown[], container: Conta
230
271
  * error first, as `createContainer` does.
231
272
  */
232
273
  export const createApp = async (root: ModuleImport, options: AppOptions = {}): Promise<App> => {
233
- const container = await createContainer(root, options)
234
- let enhancers: readonly unknown[] = []
274
+ const logger = options.logger === false ? new SilentLogger() : (options.logger ?? new ConsoleLogger())
275
+ const own = logger.child('Soda')
276
+ const onError =
277
+ options.onError ??
278
+ ((error: unknown, req: Request) =>
279
+ own.error('Unhandled error while handling a request', { error, method: req.method, url: req.url }))
235
280
 
236
- try {
237
- const built = buildRoutes(container, options)
281
+ own.log('Starting application')
282
+
283
+ const container = await createContainer(root, {
284
+ ...options,
285
+ providers: [...(options.providers ?? []), provide(Logger, { useValue: logger })],
286
+ })
287
+ const enhancers: unknown[] = []
238
288
 
239
- enhancers = built.enhancers
289
+ try {
290
+ // Middleware first: it wraps everything, so it is initialized first and destroyed last.
291
+ const middleware = resolveMiddleware(options.middleware ?? [], (fn) => container.runInContext(fn), (instance) => {
292
+ enhancers.push(instance)
293
+ })
294
+ const built = buildRoutes(container, { ...options, onError })
295
+
296
+ logStartup(logger, container, built.controllers)
297
+ enhancers.push(...built.enhancers)
240
298
  await initializeAll(enhancers)
299
+ own.log('Application started')
300
+
301
+ const compiled = compileRoutes(built.routes, options)
302
+ const routes = applyMiddleware(middleware, compiled.routes, compiled.fetch, (error, req) =>
303
+ errorResponse(error, (unknown) => onError(unknown, req))
304
+ )
241
305
 
242
- return new Application(container, compileRoutes(built.routes, options), enhancers)
306
+ return new Application(container, routes, enhancers, own)
243
307
  } catch (error) {
244
308
  try {
245
309
  await destroyEverything(enhancers, container)
@@ -19,11 +19,13 @@
19
19
  import { isThenable } from '../utils/promise.ts'
20
20
  import { runInContext } from './context.ts'
21
21
  import type { InjectionContext } from './context.ts'
22
- import type { ModuleClass, ModuleImport } from './decorators.ts'
22
+ import { Module } from './decorators.ts'
23
+ import type { DynamicModule, ModuleClass, ModuleImport } from './decorators.ts'
23
24
  import { DiError } from './errors.ts'
24
25
  import { buildGraph } from './graph.ts'
25
26
  import type { ModuleGraph, ModuleNode, ProviderNode } from './graph.ts'
26
27
  import { destroyAll, initializeAll } from './lifecycle.ts'
28
+ import { isProviderRecord } from './provider.ts'
27
29
  import type { Provider, ProviderDefinition } from './provider.ts'
28
30
  import { tokenName } from './token.ts'
29
31
  import type { Constructor, Token } from './token.ts'
@@ -43,6 +45,25 @@ export interface ContainerOptions {
43
45
  * ```
44
46
  */
45
47
  readonly overrides?: readonly Provider[]
48
+ /**
49
+ * Providers visible in every module, as if exported by a global module:
50
+ * what the application itself provides, such as its `Logger`.
51
+ */
52
+ readonly providers?: readonly Provider[]
53
+ }
54
+
55
+ /** Module holding `ContainerOptions.providers`. */
56
+ @Module({})
57
+ class ContainerProvidersModule {}
58
+
59
+ const providersModule = (providers: readonly Provider[]): DynamicModule => {
60
+ const exports: Token<unknown>[] = []
61
+
62
+ for (const provider of providers) {
63
+ exports.push(isProviderRecord(provider) ? provider.token : provider)
64
+ }
65
+
66
+ return { module: ContainerProvidersModule, providers, exports, global: true }
46
67
  }
47
68
 
48
69
  /**
@@ -81,6 +102,8 @@ export interface Container {
81
102
  runInContext<R>(fn: () => R, options?: ResolveOptions): R
82
103
  /** Every controller, in module order. */
83
104
  readonly controllers: readonly ControllerEntry[]
105
+ /** Every module, imports before importers. */
106
+ readonly modules: readonly ModuleClass[]
84
107
  /**
85
108
  * Calls `onDestroy` in reverse creation order, then releases every instance.
86
109
  * Calling it again, even from an `onDestroy` hook, returns the same promise.
@@ -398,6 +421,18 @@ class StartedContainer implements Container {
398
421
  this.controllers = controllers
399
422
  }
400
423
 
424
+ get modules(): readonly ModuleClass[] {
425
+ const modules: ModuleClass[] = []
426
+
427
+ for (const node of this.#graph.modules) {
428
+ if (node.moduleClass !== ContainerProvidersModule) {
429
+ modules.push(node.moduleClass)
430
+ }
431
+ }
432
+
433
+ return modules
434
+ }
435
+
401
436
  get<T>(token: Token<T>, options?: ResolveOptions & { readonly optional?: false }): T
402
437
  get<T>(token: Token<T>, options: ResolveOptions & { readonly optional: true }): T | undefined
403
438
  get<T>(token: Token<T>, options: ResolveOptions & { readonly optional?: boolean } = {}): T | undefined {
@@ -524,7 +559,8 @@ const createAll = async (graph: ModuleGraph, resolver: Resolver): Promise<Contro
524
559
  * hooks fail too, everything is rethrown as one `AggregateError`, startup error first.
525
560
  */
526
561
  export const createContainer = async (root: ModuleImport, options: ContainerOptions = {}): Promise<Container> => {
527
- const graph = await buildGraph(root, options.overrides ?? [])
562
+ const providers = options.providers ?? []
563
+ const graph = await buildGraph(root, options.overrides ?? [], providers.length > 0 ? [providersModule(providers)] : [])
528
564
  const resolver = new Resolver(graph)
529
565
 
530
566
  try {
package/src/di/graph.ts CHANGED
@@ -139,7 +139,17 @@ class GraphBuilder {
139
139
  }
140
140
  }
141
141
 
142
- async build(root: ModuleImport): Promise<ModuleGraph> {
142
+ /**
143
+ * `globals` are global modules added next to `root` (the container's own
144
+ * providers); they are created first, before anything that may inject them.
145
+ */
146
+ async build(root: ModuleImport, globals: readonly ModuleImport[]): Promise<ModuleGraph> {
147
+ const globalNodes: ModuleNode[] = []
148
+
149
+ for (let i = 0, len = globals.length; i < len; i++) {
150
+ globalNodes.push(await this.#module(globals[i], 'createContainer()', i))
151
+ }
152
+
143
153
  const rootNode = await this.#module(root, 'createContainer()', 0)
144
154
 
145
155
  // The queue grows while modules are read: breadth-first over imports.
@@ -149,8 +159,8 @@ class GraphBuilder {
149
159
 
150
160
  this.#assertReplacementsUsed()
151
161
 
152
- const modules = orderModules(rootNode)
153
- const globals: ModuleNode[] = []
162
+ const modules = orderModules([...globalNodes, rootNode])
163
+ const globalModules: ModuleNode[] = []
154
164
  const providers: ProviderNode[] = []
155
165
  const controllers: ProviderNode[] = []
156
166
 
@@ -160,14 +170,14 @@ class GraphBuilder {
160
170
  assertExportsProvided(node)
161
171
 
162
172
  if (node.global) {
163
- globals.push(node)
173
+ globalModules.push(node)
164
174
  }
165
175
 
166
176
  providers.push(...node.providers.values())
167
177
  controllers.push(...node.controllers)
168
178
  }
169
179
 
170
- return { root: rootNode, modules, globals, providers, controllers }
180
+ return { root: rootNode, modules, globals: globalModules, providers, controllers }
171
181
  }
172
182
 
173
183
  /** Node for an `imports` entry, created on first sight. */
@@ -320,8 +330,8 @@ const assertExportsProvided = (node: ModuleNode): void => {
320
330
  }
321
331
  }
322
332
 
323
- /** Modules reachable from `root`, each after the modules it imports. */
324
- const orderModules = (root: ModuleNode): ModuleNode[] => {
333
+ /** Modules reachable from `roots`, each after the modules it imports. */
334
+ const orderModules = (roots: readonly ModuleNode[]): ModuleNode[] => {
325
335
  const ordered: ModuleNode[] = []
326
336
  const visited = new Set<ModuleNode>()
327
337
 
@@ -339,15 +349,20 @@ const orderModules = (root: ModuleNode): ModuleNode[] => {
339
349
  ordered.push(node)
340
350
  }
341
351
 
342
- visit(root)
352
+ for (const root of roots) {
353
+ visit(root)
354
+ }
343
355
 
344
356
  return ordered
345
357
  }
346
358
 
347
359
  /**
348
- * Builds the module graph rooted at `root`.
360
+ * Builds the module graph rooted at `root`, with `globals` as extra global modules.
349
361
  *
350
362
  * `overrides` replace the provider of their token in every module providing it (for tests).
351
363
  */
352
- export const buildGraph = (root: ModuleImport, overrides: readonly Provider[]): Promise<ModuleGraph> =>
353
- new GraphBuilder(overrides).build(root)
364
+ export const buildGraph = (
365
+ root: ModuleImport,
366
+ overrides: readonly Provider[],
367
+ globals: readonly ModuleImport[] = []
368
+ ): Promise<ModuleGraph> => new GraphBuilder(overrides).build(root, globals)