@mantiq/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +65 -0
- package/src/application/Application.ts +241 -0
- package/src/cache/CacheManager.ts +180 -0
- package/src/cache/FileCacheStore.ts +113 -0
- package/src/cache/MemcachedCacheStore.ts +115 -0
- package/src/cache/MemoryCacheStore.ts +62 -0
- package/src/cache/NullCacheStore.ts +39 -0
- package/src/cache/RedisCacheStore.ts +125 -0
- package/src/cache/events.ts +52 -0
- package/src/config/ConfigRepository.ts +115 -0
- package/src/config/env.ts +26 -0
- package/src/container/Container.ts +198 -0
- package/src/container/ContextualBindingBuilder.ts +21 -0
- package/src/contracts/Cache.ts +49 -0
- package/src/contracts/Config.ts +24 -0
- package/src/contracts/Container.ts +68 -0
- package/src/contracts/DriverManager.ts +16 -0
- package/src/contracts/Encrypter.ts +32 -0
- package/src/contracts/EventDispatcher.ts +32 -0
- package/src/contracts/ExceptionHandler.ts +20 -0
- package/src/contracts/Hasher.ts +19 -0
- package/src/contracts/Middleware.ts +23 -0
- package/src/contracts/Request.ts +54 -0
- package/src/contracts/Response.ts +19 -0
- package/src/contracts/Router.ts +62 -0
- package/src/contracts/ServiceProvider.ts +31 -0
- package/src/contracts/Session.ts +47 -0
- package/src/encryption/Encrypter.ts +197 -0
- package/src/encryption/errors.ts +30 -0
- package/src/errors/ConfigKeyNotFoundError.ts +7 -0
- package/src/errors/ContainerResolutionError.ts +13 -0
- package/src/errors/ForbiddenError.ts +7 -0
- package/src/errors/HttpError.ts +16 -0
- package/src/errors/MantiqError.ts +16 -0
- package/src/errors/NotFoundError.ts +7 -0
- package/src/errors/TokenMismatchError.ts +10 -0
- package/src/errors/TooManyRequestsError.ts +10 -0
- package/src/errors/UnauthorizedError.ts +7 -0
- package/src/errors/ValidationError.ts +10 -0
- package/src/exceptions/DevErrorPage.ts +564 -0
- package/src/exceptions/Handler.ts +118 -0
- package/src/hashing/Argon2Hasher.ts +46 -0
- package/src/hashing/BcryptHasher.ts +36 -0
- package/src/hashing/HashManager.ts +80 -0
- package/src/helpers/abort.ts +46 -0
- package/src/helpers/app.ts +17 -0
- package/src/helpers/cache.ts +12 -0
- package/src/helpers/config.ts +15 -0
- package/src/helpers/encrypt.ts +22 -0
- package/src/helpers/env.ts +1 -0
- package/src/helpers/hash.ts +20 -0
- package/src/helpers/response.ts +69 -0
- package/src/helpers/route.ts +24 -0
- package/src/helpers/session.ts +11 -0
- package/src/http/Cookie.ts +26 -0
- package/src/http/Kernel.ts +252 -0
- package/src/http/Request.ts +249 -0
- package/src/http/Response.ts +112 -0
- package/src/http/UploadedFile.ts +56 -0
- package/src/index.ts +97 -0
- package/src/macroable/Macroable.ts +174 -0
- package/src/middleware/Cors.ts +91 -0
- package/src/middleware/EncryptCookies.ts +101 -0
- package/src/middleware/Pipeline.ts +66 -0
- package/src/middleware/StartSession.ts +90 -0
- package/src/middleware/TrimStrings.ts +32 -0
- package/src/middleware/VerifyCsrfToken.ts +130 -0
- package/src/providers/CoreServiceProvider.ts +97 -0
- package/src/routing/ResourceRegistrar.ts +64 -0
- package/src/routing/Route.ts +40 -0
- package/src/routing/RouteCollection.ts +50 -0
- package/src/routing/RouteMatcher.ts +92 -0
- package/src/routing/Router.ts +280 -0
- package/src/routing/events.ts +19 -0
- package/src/session/SessionManager.ts +75 -0
- package/src/session/Store.ts +192 -0
- package/src/session/handlers/CookieSessionHandler.ts +42 -0
- package/src/session/handlers/FileSessionHandler.ts +79 -0
- package/src/session/handlers/MemorySessionHandler.ts +35 -0
- package/src/websocket/WebSocketContext.ts +20 -0
- package/src/websocket/WebSocketKernel.ts +60 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
|
|
3
|
+
export interface MemcachedCacheConfig {
|
|
4
|
+
/** Comma-separated server list, e.g. "host1:11211,host2:11211" */
|
|
5
|
+
servers?: string
|
|
6
|
+
prefix?: string
|
|
7
|
+
username?: string
|
|
8
|
+
password?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Memcached-backed cache store using memjs.
|
|
13
|
+
*
|
|
14
|
+
* Requires `memjs` as a peer dependency:
|
|
15
|
+
* bun add memjs
|
|
16
|
+
*/
|
|
17
|
+
export class MemcachedCacheStore implements CacheStore {
|
|
18
|
+
private client: any
|
|
19
|
+
private readonly prefix: string
|
|
20
|
+
|
|
21
|
+
constructor(config: MemcachedCacheConfig = {}) {
|
|
22
|
+
this.prefix = config.prefix ?? 'mantiq_cache:'
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const memjs = require('memjs')
|
|
26
|
+
this.client = memjs.Client.create(config.servers ?? '127.0.0.1:11211', {
|
|
27
|
+
username: config.username,
|
|
28
|
+
password: config.password,
|
|
29
|
+
})
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'memjs is required for the Memcached cache driver. Install it with: bun add memjs',
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
38
|
+
const result = await this.client.get(this.prefixed(key))
|
|
39
|
+
if (!result.value) return undefined
|
|
40
|
+
|
|
41
|
+
const raw = result.value.toString()
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(raw) as T
|
|
44
|
+
} catch {
|
|
45
|
+
return raw as T
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async put(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
50
|
+
const serialized = JSON.stringify(value)
|
|
51
|
+
await this.client.set(this.prefixed(key), serialized, { expires: ttl ?? 0 })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async forget(key: string): Promise<boolean> {
|
|
55
|
+
return this.client.delete(this.prefixed(key))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async has(key: string): Promise<boolean> {
|
|
59
|
+
const result = await this.client.get(this.prefixed(key))
|
|
60
|
+
return result.value !== null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async flush(): Promise<void> {
|
|
64
|
+
await this.client.flush()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async increment(key: string, value = 1): Promise<number> {
|
|
68
|
+
// Memcached INCR requires the key to exist; if it doesn't, initialize it
|
|
69
|
+
try {
|
|
70
|
+
const result = await this.client.increment(this.prefixed(key), value, { initial: value })
|
|
71
|
+
return result.value ?? value
|
|
72
|
+
} catch {
|
|
73
|
+
await this.put(key, value)
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async decrement(key: string, value = 1): Promise<number> {
|
|
79
|
+
try {
|
|
80
|
+
const result = await this.client.decrement(this.prefixed(key), value, { initial: 0 })
|
|
81
|
+
return result.value ?? 0
|
|
82
|
+
} catch {
|
|
83
|
+
await this.put(key, 0)
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
89
|
+
const serialized = JSON.stringify(value)
|
|
90
|
+
try {
|
|
91
|
+
await this.client.add(this.prefixed(key), serialized, { expires: ttl ?? 0 })
|
|
92
|
+
return true
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the underlying memjs client.
|
|
100
|
+
*/
|
|
101
|
+
getClient(): any {
|
|
102
|
+
return this.client
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Close the Memcached connection.
|
|
107
|
+
*/
|
|
108
|
+
async disconnect(): Promise<void> {
|
|
109
|
+
this.client.close()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private prefixed(key: string): string {
|
|
113
|
+
return this.prefix + key
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
value: unknown
|
|
5
|
+
expiresAt: number | null // timestamp in ms, null = forever
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-memory cache store. Fast, but lost on process restart.
|
|
10
|
+
* Ideal for dev, testing, and short-lived caches.
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryCacheStore implements CacheStore {
|
|
13
|
+
private store = new Map<string, CacheEntry>()
|
|
14
|
+
|
|
15
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
16
|
+
const entry = this.store.get(key)
|
|
17
|
+
if (!entry) return undefined
|
|
18
|
+
|
|
19
|
+
if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
|
|
20
|
+
this.store.delete(key)
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return entry.value as T
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async put(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
28
|
+
this.store.set(key, {
|
|
29
|
+
value,
|
|
30
|
+
expiresAt: ttl != null ? Date.now() + ttl * 1000 : null,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async forget(key: string): Promise<boolean> {
|
|
35
|
+
return this.store.delete(key)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async has(key: string): Promise<boolean> {
|
|
39
|
+
return (await this.get(key)) !== undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async flush(): Promise<void> {
|
|
43
|
+
this.store.clear()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async increment(key: string, value = 1): Promise<number> {
|
|
47
|
+
const current = await this.get<number>(key)
|
|
48
|
+
const newValue = (current ?? 0) + value
|
|
49
|
+
await this.put(key, newValue)
|
|
50
|
+
return newValue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async decrement(key: string, value = 1): Promise<number> {
|
|
54
|
+
return this.increment(key, -value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
58
|
+
if (await this.has(key)) return false
|
|
59
|
+
await this.put(key, value, ttl)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Null cache store — never stores anything.
|
|
5
|
+
* Useful for disabling cache in testing or specific environments.
|
|
6
|
+
*/
|
|
7
|
+
export class NullCacheStore implements CacheStore {
|
|
8
|
+
async get<T = unknown>(_key: string): Promise<T | undefined> {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async put(_key: string, _value: unknown, _ttl?: number): Promise<void> {
|
|
13
|
+
// noop
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async forget(_key: string): Promise<boolean> {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async has(_key: string): Promise<boolean> {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async flush(): Promise<void> {
|
|
25
|
+
// noop
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async increment(_key: string, value = 1): Promise<number> {
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async decrement(_key: string, value = 1): Promise<number> {
|
|
33
|
+
return -value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async add(_key: string, _value: unknown, _ttl?: number): Promise<boolean> {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
|
|
3
|
+
export interface RedisCacheConfig {
|
|
4
|
+
host?: string
|
|
5
|
+
port?: number
|
|
6
|
+
password?: string
|
|
7
|
+
db?: number
|
|
8
|
+
prefix?: string
|
|
9
|
+
url?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Redis-backed cache store using ioredis.
|
|
14
|
+
*
|
|
15
|
+
* Requires `ioredis` as a peer dependency:
|
|
16
|
+
* bun add ioredis
|
|
17
|
+
*/
|
|
18
|
+
export class RedisCacheStore implements CacheStore {
|
|
19
|
+
private client: any
|
|
20
|
+
private readonly prefix: string
|
|
21
|
+
|
|
22
|
+
constructor(config: RedisCacheConfig = {}) {
|
|
23
|
+
this.prefix = config.prefix ?? 'mantiq_cache:'
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const Redis = require('ioredis')
|
|
27
|
+
if (config.url) {
|
|
28
|
+
this.client = new Redis(config.url)
|
|
29
|
+
} else {
|
|
30
|
+
this.client = new Redis({
|
|
31
|
+
host: config.host ?? '127.0.0.1',
|
|
32
|
+
port: config.port ?? 6379,
|
|
33
|
+
password: config.password,
|
|
34
|
+
db: config.db ?? 0,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'ioredis is required for the Redis cache driver. Install it with: bun add ioredis',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
45
|
+
const raw = await this.client.get(this.prefixed(key))
|
|
46
|
+
if (raw === null) return undefined
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw) as T
|
|
50
|
+
} catch {
|
|
51
|
+
return raw as T
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async put(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
56
|
+
const serialized = JSON.stringify(value)
|
|
57
|
+
if (ttl != null) {
|
|
58
|
+
await this.client.setex(this.prefixed(key), ttl, serialized)
|
|
59
|
+
} else {
|
|
60
|
+
await this.client.set(this.prefixed(key), serialized)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async forget(key: string): Promise<boolean> {
|
|
65
|
+
const count = await this.client.del(this.prefixed(key))
|
|
66
|
+
return count > 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async has(key: string): Promise<boolean> {
|
|
70
|
+
const exists = await this.client.exists(this.prefixed(key))
|
|
71
|
+
return exists > 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async flush(): Promise<void> {
|
|
75
|
+
const pattern = this.prefixed('*')
|
|
76
|
+
let cursor = '0'
|
|
77
|
+
|
|
78
|
+
do {
|
|
79
|
+
const [nextCursor, keys] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', 1000)
|
|
80
|
+
cursor = nextCursor
|
|
81
|
+
if (keys.length > 0) {
|
|
82
|
+
await this.client.del(...keys)
|
|
83
|
+
}
|
|
84
|
+
} while (cursor !== '0')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async increment(key: string, value = 1): Promise<number> {
|
|
88
|
+
return this.client.incrby(this.prefixed(key), value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async decrement(key: string, value = 1): Promise<number> {
|
|
92
|
+
return this.client.decrby(this.prefixed(key), value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
96
|
+
const serialized = JSON.stringify(value)
|
|
97
|
+
|
|
98
|
+
if (ttl != null) {
|
|
99
|
+
// SET key value EX ttl NX — only set if not exists
|
|
100
|
+
const result = await this.client.set(this.prefixed(key), serialized, 'EX', ttl, 'NX')
|
|
101
|
+
return result === 'OK'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await this.client.set(this.prefixed(key), serialized, 'NX')
|
|
105
|
+
return result === 'OK'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the underlying ioredis client for advanced operations.
|
|
110
|
+
*/
|
|
111
|
+
getClient(): any {
|
|
112
|
+
return this.client
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Disconnect the Redis client.
|
|
117
|
+
*/
|
|
118
|
+
async disconnect(): Promise<void> {
|
|
119
|
+
await this.client.quit()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private prefixed(key: string): string {
|
|
123
|
+
return this.prefix + key
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Event } from '../contracts/EventDispatcher.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fired when a cache key is found (cache hit).
|
|
5
|
+
*/
|
|
6
|
+
export class CacheHit extends Event {
|
|
7
|
+
constructor(
|
|
8
|
+
public readonly key: string,
|
|
9
|
+
public readonly value: unknown,
|
|
10
|
+
public readonly store: string,
|
|
11
|
+
) {
|
|
12
|
+
super()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fired when a cache key is not found (cache miss).
|
|
18
|
+
*/
|
|
19
|
+
export class CacheMissed extends Event {
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly key: string,
|
|
22
|
+
public readonly store: string,
|
|
23
|
+
) {
|
|
24
|
+
super()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fired when a value is written to the cache.
|
|
30
|
+
*/
|
|
31
|
+
export class KeyWritten extends Event {
|
|
32
|
+
constructor(
|
|
33
|
+
public readonly key: string,
|
|
34
|
+
public readonly value: unknown,
|
|
35
|
+
public readonly ttl: number | undefined,
|
|
36
|
+
public readonly store: string,
|
|
37
|
+
) {
|
|
38
|
+
super()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fired when a cache key is removed.
|
|
44
|
+
*/
|
|
45
|
+
export class KeyForgotten extends Event {
|
|
46
|
+
constructor(
|
|
47
|
+
public readonly key: string,
|
|
48
|
+
public readonly store: string,
|
|
49
|
+
) {
|
|
50
|
+
super()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Config } from '../contracts/Config.ts'
|
|
2
|
+
import { ConfigKeyNotFoundError } from '../errors/ConfigKeyNotFoundError.ts'
|
|
3
|
+
|
|
4
|
+
export class ConfigRepository implements Config {
|
|
5
|
+
private data: Record<string, any> = {}
|
|
6
|
+
|
|
7
|
+
constructor(data: Record<string, any> = {}) {
|
|
8
|
+
this.data = data
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a config value using dot-notation.
|
|
13
|
+
* @throws ConfigKeyNotFoundError if key doesn't exist and no default is provided
|
|
14
|
+
*/
|
|
15
|
+
get<T = any>(key: string, defaultValue?: T): T {
|
|
16
|
+
const value = this.getByDotNotation(this.data, key)
|
|
17
|
+
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
if (defaultValue !== undefined) return defaultValue
|
|
20
|
+
throw new ConfigKeyNotFoundError(key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value as T
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(key: string, value: any): void {
|
|
27
|
+
this.setByDotNotation(this.data, key, value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
has(key: string): boolean {
|
|
31
|
+
return this.getByDotNotation(this.data, key) !== undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
all(): Record<string, any> {
|
|
35
|
+
return { ...this.data }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load config from a directory of TypeScript/JS config files.
|
|
40
|
+
* Each file's default export becomes a top-level key named after the file.
|
|
41
|
+
*/
|
|
42
|
+
static async fromDirectory(configPath: string): Promise<ConfigRepository> {
|
|
43
|
+
const data: Record<string, any> = {}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const glob = new Bun.Glob('*.ts')
|
|
47
|
+
for await (const file of glob.scan(configPath)) {
|
|
48
|
+
const key = file.replace(/\.ts$/, '')
|
|
49
|
+
try {
|
|
50
|
+
const mod = await import(`${configPath}/${file}`)
|
|
51
|
+
data[key] = mod.default ?? mod
|
|
52
|
+
} catch {
|
|
53
|
+
// Skip files that fail to load
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Config directory doesn't exist — use empty config
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new ConfigRepository(data)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load from a cached JSON file (production optimization).
|
|
65
|
+
*/
|
|
66
|
+
static fromCache(cachePath: string): ConfigRepository {
|
|
67
|
+
try {
|
|
68
|
+
const file = Bun.file(cachePath)
|
|
69
|
+
const json = JSON.parse(new TextDecoder().decode(file.arrayBuffer() as any))
|
|
70
|
+
return new ConfigRepository(json)
|
|
71
|
+
} catch {
|
|
72
|
+
return new ConfigRepository()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
private getByDotNotation(obj: Record<string, any>, key: string): any {
|
|
79
|
+
if (!key.includes('.')) {
|
|
80
|
+
return obj[key]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parts = key.split('.')
|
|
84
|
+
let current: any = obj
|
|
85
|
+
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
current = current[part]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return current
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private setByDotNotation(obj: Record<string, any>, key: string, value: any): void {
|
|
97
|
+
if (!key.includes('.')) {
|
|
98
|
+
obj[key] = value
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parts = key.split('.')
|
|
103
|
+
let current = obj
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
106
|
+
const part = parts[i]!
|
|
107
|
+
if (current[part] === undefined || typeof current[part] !== 'object') {
|
|
108
|
+
current[part] = {}
|
|
109
|
+
}
|
|
110
|
+
current = current[part]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
current[parts[parts.length - 1]!] = value
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read an environment variable with type coercion.
|
|
3
|
+
*
|
|
4
|
+
* Type coercion rules:
|
|
5
|
+
* - 'true' / 'false' → boolean
|
|
6
|
+
* - '' (empty string) → '' (not undefined)
|
|
7
|
+
* - undefined → defaultValue
|
|
8
|
+
* - All other strings remain strings
|
|
9
|
+
*
|
|
10
|
+
* @example env('APP_DEBUG', false)
|
|
11
|
+
* @example env('APP_NAME', 'MantiqJS')
|
|
12
|
+
*/
|
|
13
|
+
export function env<T = string>(key: string, defaultValue?: T): T {
|
|
14
|
+
const raw = process.env[key]
|
|
15
|
+
|
|
16
|
+
if (raw === undefined) {
|
|
17
|
+
if (defaultValue !== undefined) return defaultValue
|
|
18
|
+
return undefined as unknown as T
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Boolean coercion
|
|
22
|
+
if (raw === 'true') return true as unknown as T
|
|
23
|
+
if (raw === 'false') return false as unknown as T
|
|
24
|
+
|
|
25
|
+
return raw as unknown as T
|
|
26
|
+
}
|