@strav/kernel 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/package.json +59 -0
- package/src/cache/cache_manager.ts +60 -0
- package/src/cache/cache_store.ts +31 -0
- package/src/cache/helpers.ts +74 -0
- package/src/cache/index.ts +4 -0
- package/src/cache/memory_store.ts +63 -0
- package/src/config/configuration.ts +105 -0
- package/src/config/index.ts +2 -0
- package/src/config/loaders/base_loader.ts +69 -0
- package/src/config/loaders/env_loader.ts +112 -0
- package/src/config/loaders/typescript_loader.ts +56 -0
- package/src/config/types.ts +8 -0
- package/src/core/application.ts +241 -0
- package/src/core/container.ts +113 -0
- package/src/core/index.ts +4 -0
- package/src/core/inject.ts +39 -0
- package/src/core/service_provider.ts +44 -0
- package/src/encryption/encryption_manager.ts +215 -0
- package/src/encryption/helpers.ts +158 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/types.ts +6 -0
- package/src/events/emitter.ts +101 -0
- package/src/events/index.ts +2 -0
- package/src/exceptions/errors.ts +71 -0
- package/src/exceptions/exception_handler.ts +140 -0
- package/src/exceptions/helpers.ts +25 -0
- package/src/exceptions/http_exception.ts +132 -0
- package/src/exceptions/index.ts +23 -0
- package/src/exceptions/strav_error.ts +11 -0
- package/src/helpers/compose.ts +104 -0
- package/src/helpers/crypto.ts +4 -0
- package/src/helpers/env.ts +50 -0
- package/src/helpers/index.ts +6 -0
- package/src/helpers/strings.ts +67 -0
- package/src/helpers/ulid.ts +28 -0
- package/src/i18n/defaults/en/validation.json +20 -0
- package/src/i18n/helpers.ts +76 -0
- package/src/i18n/i18n_manager.ts +157 -0
- package/src/i18n/index.ts +3 -0
- package/src/i18n/translator.ts +96 -0
- package/src/i18n/types.ts +17 -0
- package/src/index.ts +11 -0
- package/src/logger/index.ts +5 -0
- package/src/logger/logger.ts +113 -0
- package/src/logger/sinks/console_sink.ts +24 -0
- package/src/logger/sinks/file_sink.ts +24 -0
- package/src/logger/sinks/sink.ts +36 -0
- package/src/providers/cache_provider.ts +16 -0
- package/src/providers/config_provider.ts +26 -0
- package/src/providers/encryption_provider.ts +16 -0
- package/src/providers/i18n_provider.ts +17 -0
- package/src/providers/index.ts +8 -0
- package/src/providers/logger_provider.ts +16 -0
- package/src/providers/storage_provider.ts +16 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/local_driver.ts +46 -0
- package/src/storage/ostra_client.ts +432 -0
- package/src/storage/ostra_driver.ts +58 -0
- package/src/storage/s3_driver.ts +51 -0
- package/src/storage/storage.ts +43 -0
- package/src/storage/storage_manager.ts +70 -0
- package/src/storage/types.ts +49 -0
- package/src/storage/upload.ts +91 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import pino from 'pino'
|
|
2
|
+
import Configuration from '../config/configuration.ts'
|
|
3
|
+
import { inject } from '../core/inject.ts'
|
|
4
|
+
import Emitter from '../events/emitter.ts'
|
|
5
|
+
import { LogSink, type SinkConfig } from './sinks/sink'
|
|
6
|
+
import { ConsoleSink } from './sinks/console_sink'
|
|
7
|
+
import { FileSink } from './sinks/file_sink'
|
|
8
|
+
|
|
9
|
+
type SinkConstructor = new (config: SinkConfig) => LogSink
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps sink names used in `config/logging.ts` to their implementing classes.
|
|
13
|
+
* Register new sink types here so the Logger can instantiate them from config.
|
|
14
|
+
*/
|
|
15
|
+
const sinkRegistry: Record<string, SinkConstructor> = {
|
|
16
|
+
console: ConsoleSink,
|
|
17
|
+
file: FileSink,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Structured logger backed by pino with configurable sinks.
|
|
22
|
+
*
|
|
23
|
+
* Sinks (console, file, …) are declared in `config/logging.ts` and combined
|
|
24
|
+
* at construction time via `pino.multistream`. Each sink can define its own
|
|
25
|
+
* minimum log level independently of the global level.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Resolve via DI (Configuration is injected automatically):
|
|
29
|
+
* container.singleton(Logger)
|
|
30
|
+
* const logger = container.resolve(Logger)
|
|
31
|
+
*
|
|
32
|
+
* logger.info('server started', { port: 3000 })
|
|
33
|
+
* logger.error('request failed', { statusCode: 500, path: '/api/users' })
|
|
34
|
+
*/
|
|
35
|
+
@inject
|
|
36
|
+
export default class Logger {
|
|
37
|
+
private pino: pino.Logger
|
|
38
|
+
|
|
39
|
+
constructor(protected config: Configuration) {
|
|
40
|
+
const sinks = this.buildSinks()
|
|
41
|
+
|
|
42
|
+
const streams = sinks.map(sink => ({
|
|
43
|
+
stream: sink.createStream(),
|
|
44
|
+
level: sink.level as pino.Level,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const level = this.config.get('logging.level', 'info') as string satisfies string
|
|
48
|
+
|
|
49
|
+
this.pino = streams.length > 0 ? pino({ level }, pino.multistream(streams)) : pino({ level })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Log at `trace` level (most verbose). */
|
|
53
|
+
trace(msg: string, context?: Record<string, unknown>): void {
|
|
54
|
+
context ? this.pino.trace(context, msg) : this.pino.trace(msg)
|
|
55
|
+
this.emitLog('trace', msg, context)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Log at `debug` level. */
|
|
59
|
+
debug(msg: string, context?: Record<string, unknown>): void {
|
|
60
|
+
context ? this.pino.debug(context, msg) : this.pino.debug(msg)
|
|
61
|
+
this.emitLog('debug', msg, context)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Log at `info` level. */
|
|
65
|
+
info(msg: string, context?: Record<string, unknown>): void {
|
|
66
|
+
context ? this.pino.info(context, msg) : this.pino.info(msg)
|
|
67
|
+
this.emitLog('info', msg, context)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Log at `warn` level. */
|
|
71
|
+
warn(msg: string, context?: Record<string, unknown>): void {
|
|
72
|
+
context ? this.pino.warn(context, msg) : this.pino.warn(msg)
|
|
73
|
+
this.emitLog('warn', msg, context)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Log at `error` level. */
|
|
77
|
+
error(msg: string, context?: Record<string, unknown>): void {
|
|
78
|
+
context ? this.pino.error(context, msg) : this.pino.error(msg)
|
|
79
|
+
this.emitLog('error', msg, context)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Log at `fatal` level (most severe). */
|
|
83
|
+
fatal(msg: string, context?: Record<string, unknown>): void {
|
|
84
|
+
context ? this.pino.fatal(context, msg) : this.pino.fatal(msg)
|
|
85
|
+
this.emitLog('fatal', msg, context)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Emit a log event for devtools/observability consumers. Fire-and-forget. */
|
|
89
|
+
private emitLog(level: string, msg: string, context?: Record<string, unknown>): void {
|
|
90
|
+
if (Emitter.listenerCount('log:entry') === 0) return
|
|
91
|
+
Emitter.emit('log:entry', { level, msg, context }).catch(() => {})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read the `logging.sinks` configuration and instantiate every enabled sink
|
|
96
|
+
* whose name matches an entry in {@link sinkRegistry}.
|
|
97
|
+
*/
|
|
98
|
+
private buildSinks(): LogSink[] {
|
|
99
|
+
const sinks: LogSink[] = []
|
|
100
|
+
const sinksConfig = this.config.get('logging.sinks', {}) as Record<string, SinkConfig>
|
|
101
|
+
|
|
102
|
+
for (const [name, sinkConfig] of Object.entries(sinksConfig)) {
|
|
103
|
+
if (!sinkConfig.enabled) continue
|
|
104
|
+
|
|
105
|
+
const SinkClass = sinkRegistry[name]
|
|
106
|
+
if (SinkClass) {
|
|
107
|
+
sinks.push(new SinkClass(sinkConfig))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sinks
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import build from 'pino-pretty'
|
|
2
|
+
import { LogSink, type SinkConfig } from './sink'
|
|
3
|
+
|
|
4
|
+
/** Configuration specific to the console sink. */
|
|
5
|
+
export interface ConsoleSinkConfig extends SinkConfig {
|
|
6
|
+
/** Enable ANSI colour output. Defaults to `true`. */
|
|
7
|
+
colorize?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Log sink that writes human-readable output to stdout via `pino-pretty`.
|
|
12
|
+
*
|
|
13
|
+
* Intended for local development; for structured JSON output in production
|
|
14
|
+
* consider disabling this sink and using the file sink instead.
|
|
15
|
+
*/
|
|
16
|
+
export class ConsoleSink extends LogSink {
|
|
17
|
+
createStream() {
|
|
18
|
+
const config = this.config as ConsoleSinkConfig
|
|
19
|
+
|
|
20
|
+
return build({
|
|
21
|
+
colorize: config.colorize ?? true,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import pino from 'pino'
|
|
4
|
+
import { LogSink, type SinkConfig } from './sink'
|
|
5
|
+
|
|
6
|
+
/** Configuration specific to the file sink. */
|
|
7
|
+
export interface FileSinkConfig extends SinkConfig {
|
|
8
|
+
/** Path to the log file. Parent directories are created automatically. */
|
|
9
|
+
path: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Log sink that writes structured JSON to a file using pino's optimised
|
|
14
|
+
* {@link pino.destination | SonicBoom} writer with asynchronous flushing.
|
|
15
|
+
*/
|
|
16
|
+
export class FileSink extends LogSink {
|
|
17
|
+
createStream() {
|
|
18
|
+
const { path } = this.config as FileSinkConfig
|
|
19
|
+
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
21
|
+
|
|
22
|
+
return pino.destination({ dest: path, sync: false })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DestinationStream } from 'pino'
|
|
2
|
+
|
|
3
|
+
/** Supported log severity levels, from most to least verbose. */
|
|
4
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'
|
|
5
|
+
|
|
6
|
+
/** Configuration object for a single log sink. */
|
|
7
|
+
export interface SinkConfig {
|
|
8
|
+
/** Whether this sink is active. Disabled sinks are skipped by the Logger. */
|
|
9
|
+
enabled: boolean
|
|
10
|
+
/** Minimum severity level this sink will accept. Defaults to `"info"`. */
|
|
11
|
+
level?: LogLevel
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Abstract base class for log sinks.
|
|
17
|
+
*
|
|
18
|
+
* Subclasses implement {@link createStream} to provide a writable destination
|
|
19
|
+
* (stdout, file, network, etc.) that the Logger combines via pino multistream.
|
|
20
|
+
*
|
|
21
|
+
* To add a new sink:
|
|
22
|
+
* 1. Extend this class and implement `createStream()`.
|
|
23
|
+
* 2. Register the class in the `sinkRegistry` inside `logger.ts`.
|
|
24
|
+
* 3. Add a corresponding section in `config/logging.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export abstract class LogSink {
|
|
27
|
+
constructor(protected config: SinkConfig) {}
|
|
28
|
+
|
|
29
|
+
/** Create the writable destination stream for this sink. */
|
|
30
|
+
abstract createStream(): DestinationStream
|
|
31
|
+
|
|
32
|
+
/** Minimum log level for this sink. Defaults to `"info"` when not configured. */
|
|
33
|
+
get level(): LogLevel {
|
|
34
|
+
return this.config.level ?? 'info'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import CacheManager from '../cache/cache_manager.ts'
|
|
4
|
+
|
|
5
|
+
export default class CacheProvider extends ServiceProvider {
|
|
6
|
+
readonly name = 'cache'
|
|
7
|
+
override readonly dependencies = ['config']
|
|
8
|
+
|
|
9
|
+
override register(app: Application): void {
|
|
10
|
+
app.singleton(CacheManager)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override boot(app: Application): void {
|
|
14
|
+
app.resolve(CacheManager)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import Configuration from '../config/configuration.ts'
|
|
4
|
+
|
|
5
|
+
export interface ConfigProviderOptions {
|
|
6
|
+
/** Path to the config directory. Default: `'./config'` */
|
|
7
|
+
directory?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default class ConfigProvider extends ServiceProvider {
|
|
11
|
+
readonly name = 'config'
|
|
12
|
+
|
|
13
|
+
constructor(private options?: ConfigProviderOptions) {
|
|
14
|
+
super()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override register(app: Application): void {
|
|
18
|
+
const dir = this.options?.directory ?? './config'
|
|
19
|
+
app.singleton(Configuration, () => new Configuration(dir))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override async boot(app: Application): Promise<void> {
|
|
23
|
+
const config = app.resolve(Configuration)
|
|
24
|
+
await config.load()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import EncryptionManager from '../encryption/encryption_manager.ts'
|
|
4
|
+
|
|
5
|
+
export default class EncryptionProvider extends ServiceProvider {
|
|
6
|
+
readonly name = 'encryption'
|
|
7
|
+
override readonly dependencies = ['config']
|
|
8
|
+
|
|
9
|
+
override register(app: Application): void {
|
|
10
|
+
app.singleton(EncryptionManager)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override boot(app: Application): void {
|
|
14
|
+
app.resolve(EncryptionManager)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import I18nManager from '../i18n/i18n_manager.ts'
|
|
4
|
+
|
|
5
|
+
export default class I18nProvider extends ServiceProvider {
|
|
6
|
+
readonly name = 'i18n'
|
|
7
|
+
override readonly dependencies = ['config']
|
|
8
|
+
|
|
9
|
+
override register(app: Application): void {
|
|
10
|
+
app.singleton(I18nManager)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async boot(app: Application): Promise<void> {
|
|
14
|
+
app.resolve(I18nManager)
|
|
15
|
+
await I18nManager.load()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as ConfigProvider } from './config_provider.ts'
|
|
2
|
+
export { default as EncryptionProvider } from './encryption_provider.ts'
|
|
3
|
+
export { default as StorageProvider } from './storage_provider.ts'
|
|
4
|
+
export { default as CacheProvider } from './cache_provider.ts'
|
|
5
|
+
export { default as I18nProvider } from './i18n_provider.ts'
|
|
6
|
+
export { default as LoggerProvider } from './logger_provider.ts'
|
|
7
|
+
|
|
8
|
+
export type { ConfigProviderOptions } from './config_provider.ts'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import Logger from '../logger/logger.ts'
|
|
4
|
+
|
|
5
|
+
export default class LoggerProvider extends ServiceProvider {
|
|
6
|
+
readonly name = 'logger'
|
|
7
|
+
override readonly dependencies = ['config']
|
|
8
|
+
|
|
9
|
+
override register(app: Application): void {
|
|
10
|
+
app.singleton(Logger)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override boot(app: Application): void {
|
|
14
|
+
app.resolve(Logger)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ServiceProvider from '../core/service_provider.ts'
|
|
2
|
+
import type Application from '../core/application.ts'
|
|
3
|
+
import StorageManager from '../storage/storage_manager.ts'
|
|
4
|
+
|
|
5
|
+
export default class StorageProvider extends ServiceProvider {
|
|
6
|
+
readonly name = 'storage'
|
|
7
|
+
override readonly dependencies = ['config']
|
|
8
|
+
|
|
9
|
+
override register(app: Application): void {
|
|
10
|
+
app.singleton(StorageManager)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override boot(app: Application): void {
|
|
14
|
+
app.resolve(StorageManager)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { default as Storage } from './storage.ts'
|
|
2
|
+
export { Upload, FileTooLargeError, InvalidFileTypeError } from './upload.ts'
|
|
3
|
+
export { default as StorageManager } from './storage_manager.ts'
|
|
4
|
+
export { default as LocalDriver } from './local_driver.ts'
|
|
5
|
+
export { default as S3Driver } from './s3_driver.ts'
|
|
6
|
+
export { default as OstraDriver } from './ostra_driver.ts'
|
|
7
|
+
export { default as OstraClient, OstraBucket, OstraMultipart, OstraError } from './ostra_client.ts'
|
|
8
|
+
export type {
|
|
9
|
+
StorageDriver,
|
|
10
|
+
StorageConfig,
|
|
11
|
+
FileStats,
|
|
12
|
+
LocalDriverConfig,
|
|
13
|
+
S3DriverConfig,
|
|
14
|
+
OstraDriverConfig,
|
|
15
|
+
} from './types.ts'
|
|
16
|
+
export type { UploadResult } from './upload.ts'
|
|
17
|
+
export type {
|
|
18
|
+
OstraClientConfig,
|
|
19
|
+
BucketInfo,
|
|
20
|
+
BucketStats,
|
|
21
|
+
ObjectMeta,
|
|
22
|
+
ObjectHeaders,
|
|
23
|
+
ListResult,
|
|
24
|
+
VersionInfo,
|
|
25
|
+
VersionListResult,
|
|
26
|
+
BatchDeleteResult,
|
|
27
|
+
SignedUrlResult,
|
|
28
|
+
MultipartInfo,
|
|
29
|
+
PartInfo,
|
|
30
|
+
TokenInfo,
|
|
31
|
+
TokenRecord,
|
|
32
|
+
} from './ostra_client.ts'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join, extname, resolve } from 'node:path'
|
|
2
|
+
import { unlink } from 'node:fs/promises'
|
|
3
|
+
import { randomHex } from '../helpers/crypto.ts'
|
|
4
|
+
import type { StorageDriver, LocalDriverConfig } from './types.ts'
|
|
5
|
+
|
|
6
|
+
export default class LocalDriver implements StorageDriver {
|
|
7
|
+
private root: string
|
|
8
|
+
private baseUrl: string
|
|
9
|
+
|
|
10
|
+
constructor(config: LocalDriverConfig) {
|
|
11
|
+
this.root = resolve(config.root)
|
|
12
|
+
this.baseUrl = config.baseUrl
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async put(directory: string, file: File, name?: string): Promise<string> {
|
|
16
|
+
const ext = extname(file.name)
|
|
17
|
+
const filename = name ?? `${randomHex(8)}${ext}`
|
|
18
|
+
const relativePath = join(directory, filename)
|
|
19
|
+
const fullPath = join(this.root, relativePath)
|
|
20
|
+
|
|
21
|
+
await Bun.write(fullPath, file)
|
|
22
|
+
|
|
23
|
+
return relativePath
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async get(path: string): Promise<Blob | null> {
|
|
27
|
+
const file = Bun.file(join(this.root, path))
|
|
28
|
+
if (!(await file.exists())) return null
|
|
29
|
+
return file
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async exists(path: string): Promise<boolean> {
|
|
33
|
+
return Bun.file(join(this.root, path)).exists()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async delete(path: string): Promise<void> {
|
|
37
|
+
const fullPath = join(this.root, path)
|
|
38
|
+
if (await Bun.file(fullPath).exists()) {
|
|
39
|
+
await unlink(fullPath)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
url(path: string): string {
|
|
44
|
+
return `${this.baseUrl}/${path}`
|
|
45
|
+
}
|
|
46
|
+
}
|