@stravigor/core 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 +45 -0
- package/package.json +83 -0
- package/src/auth/access_token.ts +122 -0
- package/src/auth/auth.ts +86 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/middleware/authenticate.ts +64 -0
- package/src/auth/middleware/csrf.ts +62 -0
- package/src/auth/middleware/guest.ts +46 -0
- package/src/broadcast/broadcast_manager.ts +411 -0
- package/src/broadcast/client.ts +302 -0
- package/src/broadcast/index.ts +58 -0
- package/src/cache/cache_manager.ts +56 -0
- package/src/cache/cache_store.ts +31 -0
- package/src/cache/helpers.ts +74 -0
- package/src/cache/http_cache.ts +109 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory_store.ts +63 -0
- package/src/cli/bootstrap.ts +37 -0
- package/src/cli/commands/generate_api.ts +74 -0
- package/src/cli/commands/generate_key.ts +46 -0
- package/src/cli/commands/generate_models.ts +48 -0
- package/src/cli/commands/migration_compare.ts +152 -0
- package/src/cli/commands/migration_fresh.ts +123 -0
- package/src/cli/commands/migration_generate.ts +79 -0
- package/src/cli/commands/migration_rollback.ts +53 -0
- package/src/cli/commands/migration_run.ts +44 -0
- package/src/cli/commands/queue_flush.ts +35 -0
- package/src/cli/commands/queue_retry.ts +34 -0
- package/src/cli/commands/queue_work.ts +40 -0
- package/src/cli/commands/scheduler_work.ts +45 -0
- package/src/cli/strav.ts +33 -0
- package/src/config/configuration.ts +105 -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 +4 -0
- package/src/core/container.ts +117 -0
- package/src/core/index.ts +3 -0
- package/src/core/inject.ts +39 -0
- package/src/database/database.ts +54 -0
- package/src/database/index.ts +30 -0
- package/src/database/introspector.ts +446 -0
- package/src/database/migration/differ.ts +308 -0
- package/src/database/migration/file_generator.ts +125 -0
- package/src/database/migration/index.ts +18 -0
- package/src/database/migration/runner.ts +133 -0
- package/src/database/migration/sql_generator.ts +378 -0
- package/src/database/migration/tracker.ts +76 -0
- package/src/database/migration/types.ts +189 -0
- package/src/database/query_builder.ts +474 -0
- package/src/encryption/encryption_manager.ts +209 -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 +75 -0
- package/src/exceptions/exception_handler.ts +126 -0
- package/src/exceptions/helpers.ts +25 -0
- package/src/exceptions/http_exception.ts +129 -0
- package/src/exceptions/index.ts +23 -0
- package/src/exceptions/strav_error.ts +11 -0
- package/src/generators/api_generator.ts +972 -0
- package/src/generators/config.ts +87 -0
- package/src/generators/doc_generator.ts +974 -0
- package/src/generators/index.ts +11 -0
- package/src/generators/model_generator.ts +586 -0
- package/src/generators/route_generator.ts +188 -0
- package/src/generators/test_generator.ts +1666 -0
- package/src/helpers/crypto.ts +4 -0
- package/src/helpers/env.ts +50 -0
- package/src/helpers/identity.ts +12 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/strings.ts +67 -0
- package/src/http/context.ts +215 -0
- package/src/http/cookie.ts +59 -0
- package/src/http/cors.ts +163 -0
- package/src/http/index.ts +16 -0
- package/src/http/middleware.ts +39 -0
- package/src/http/rate_limit.ts +173 -0
- package/src/http/router.ts +556 -0
- package/src/http/server.ts +79 -0
- package/src/i18n/defaults/en/validation.json +20 -0
- package/src/i18n/helpers.ts +72 -0
- package/src/i18n/i18n_manager.ts +155 -0
- package/src/i18n/index.ts +4 -0
- package/src/i18n/middleware.ts +90 -0
- package/src/i18n/translator.ts +96 -0
- package/src/i18n/types.ts +17 -0
- package/src/logger/index.ts +6 -0
- package/src/logger/logger.ts +100 -0
- package/src/logger/request_logger.ts +19 -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/mail/css_inliner.ts +79 -0
- package/src/mail/helpers.ts +212 -0
- package/src/mail/index.ts +19 -0
- package/src/mail/mail_manager.ts +92 -0
- package/src/mail/transports/log_transport.ts +69 -0
- package/src/mail/transports/resend_transport.ts +59 -0
- package/src/mail/transports/sendgrid_transport.ts +77 -0
- package/src/mail/transports/smtp_transport.ts +48 -0
- package/src/mail/types.ts +80 -0
- package/src/notification/base_notification.ts +67 -0
- package/src/notification/channels/database_channel.ts +30 -0
- package/src/notification/channels/discord_channel.ts +43 -0
- package/src/notification/channels/email_channel.ts +37 -0
- package/src/notification/channels/webhook_channel.ts +45 -0
- package/src/notification/helpers.ts +214 -0
- package/src/notification/index.ts +20 -0
- package/src/notification/notification_manager.ts +126 -0
- package/src/notification/types.ts +122 -0
- package/src/orm/base_model.ts +351 -0
- package/src/orm/decorators.ts +127 -0
- package/src/orm/index.ts +4 -0
- package/src/policy/authorize.ts +44 -0
- package/src/policy/index.ts +3 -0
- package/src/policy/policy_result.ts +13 -0
- package/src/queue/index.ts +11 -0
- package/src/queue/queue.ts +338 -0
- package/src/queue/worker.ts +197 -0
- package/src/scheduler/cron.ts +140 -0
- package/src/scheduler/index.ts +7 -0
- package/src/scheduler/runner.ts +116 -0
- package/src/scheduler/schedule.ts +183 -0
- package/src/scheduler/scheduler.ts +47 -0
- package/src/schema/database_representation.ts +122 -0
- package/src/schema/define_association.ts +60 -0
- package/src/schema/define_schema.ts +46 -0
- package/src/schema/field_builder.ts +155 -0
- package/src/schema/field_definition.ts +66 -0
- package/src/schema/index.ts +21 -0
- package/src/schema/naming.ts +19 -0
- package/src/schema/postgres.ts +109 -0
- package/src/schema/registry.ts +157 -0
- package/src/schema/representation_builder.ts +479 -0
- package/src/schema/type_builder.ts +107 -0
- package/src/schema/types.ts +35 -0
- package/src/session/index.ts +4 -0
- package/src/session/middleware.ts +46 -0
- package/src/session/session.ts +308 -0
- package/src/session/session_manager.ts +81 -0
- package/src/storage/index.ts +13 -0
- package/src/storage/local_driver.ts +46 -0
- package/src/storage/s3_driver.ts +51 -0
- package/src/storage/storage.ts +43 -0
- package/src/storage/storage_manager.ts +59 -0
- package/src/storage/types.ts +42 -0
- package/src/storage/upload.ts +91 -0
- package/src/validation/index.ts +18 -0
- package/src/validation/rules.ts +170 -0
- package/src/validation/validate.ts +41 -0
- package/src/view/cache.ts +47 -0
- package/src/view/client/islands.ts +50 -0
- package/src/view/compiler.ts +185 -0
- package/src/view/engine.ts +139 -0
- package/src/view/escape.ts +14 -0
- package/src/view/index.ts +13 -0
- package/src/view/islands/island_builder.ts +161 -0
- package/src/view/islands/vue_plugin.ts +140 -0
- package/src/view/middleware/static.ts +35 -0
- package/src/view/tokenizer.ts +172 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export { default, default as BroadcastManager } from './broadcast_manager.ts'
|
|
2
|
+
export type {
|
|
3
|
+
AuthorizeCallback,
|
|
4
|
+
MessageHandler,
|
|
5
|
+
ChannelConfig,
|
|
6
|
+
BootOptions,
|
|
7
|
+
PendingBroadcast,
|
|
8
|
+
} from './broadcast_manager.ts'
|
|
9
|
+
export { Broadcast, Subscription } from './client.ts'
|
|
10
|
+
export type { BroadcastOptions } from './client.ts'
|
|
11
|
+
|
|
12
|
+
import BroadcastManager from './broadcast_manager.ts'
|
|
13
|
+
import type { AuthorizeCallback, ChannelConfig, BootOptions } from './broadcast_manager.ts'
|
|
14
|
+
import type Router from '../http/router.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Broadcast helper — convenience object that delegates to `BroadcastManager`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* import { broadcast } from '@stravigor/core/broadcast'
|
|
21
|
+
*
|
|
22
|
+
* // Bootstrap
|
|
23
|
+
* broadcast.boot(router, { middleware: [session()] })
|
|
24
|
+
*
|
|
25
|
+
* // Define channels
|
|
26
|
+
* broadcast.channel('notifications')
|
|
27
|
+
* broadcast.channel('chats/:id', async (ctx, { id }) => !!ctx.get('user'))
|
|
28
|
+
*
|
|
29
|
+
* // Broadcast from anywhere
|
|
30
|
+
* broadcast.to('notifications').send('alert', { text: 'Hello' })
|
|
31
|
+
* broadcast.to(`chats/${chatId}`).except(senderId).send('message', data)
|
|
32
|
+
*/
|
|
33
|
+
export const broadcast = {
|
|
34
|
+
/** Register the broadcast WebSocket endpoint on the router. */
|
|
35
|
+
boot(router: Router, options?: BootOptions): void {
|
|
36
|
+
BroadcastManager.boot(router, options)
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/** Register a channel with optional authorization and message handlers. */
|
|
40
|
+
channel(pattern: string, config?: AuthorizeCallback | ChannelConfig): void {
|
|
41
|
+
BroadcastManager.channel(pattern, config)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/** Begin a broadcast to a channel. */
|
|
45
|
+
to(channel: string) {
|
|
46
|
+
return BroadcastManager.to(channel)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/** Number of active WebSocket connections. */
|
|
50
|
+
get clientCount() {
|
|
51
|
+
return BroadcastManager.clientCount
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/** Number of subscribers on a specific channel. */
|
|
55
|
+
subscriberCount(channel: string) {
|
|
56
|
+
return BroadcastManager.subscriberCount(channel)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { inject } from '../core/inject.ts'
|
|
2
|
+
import { ConfigurationError } from '../exceptions/errors.ts'
|
|
3
|
+
import Configuration from '../config/configuration.ts'
|
|
4
|
+
import { MemoryCacheStore } from './memory_store.ts'
|
|
5
|
+
import type { CacheStore, CacheConfig } from './cache_store.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Central cache configuration hub.
|
|
9
|
+
*
|
|
10
|
+
* Resolved once via the DI container — reads the cache config
|
|
11
|
+
* and initializes the appropriate store driver.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* app.singleton(CacheManager)
|
|
15
|
+
* app.resolve(CacheManager)
|
|
16
|
+
*
|
|
17
|
+
* // Plug in a custom store (e.g., Redis)
|
|
18
|
+
* CacheManager.useStore(new MyRedisStore())
|
|
19
|
+
*/
|
|
20
|
+
@inject
|
|
21
|
+
export default class CacheManager {
|
|
22
|
+
private static _store: CacheStore
|
|
23
|
+
private static _config: CacheConfig
|
|
24
|
+
|
|
25
|
+
constructor(config: Configuration) {
|
|
26
|
+
CacheManager._config = {
|
|
27
|
+
default: 'memory',
|
|
28
|
+
prefix: '',
|
|
29
|
+
ttl: 3600,
|
|
30
|
+
...(config.get('cache', {}) as object),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const driver = CacheManager._config.default
|
|
34
|
+
if (driver === 'memory') {
|
|
35
|
+
CacheManager._store = new MemoryCacheStore()
|
|
36
|
+
} else {
|
|
37
|
+
throw new ConfigurationError(`Unknown cache driver: ${driver}. Use CacheManager.useStore() for custom drivers.`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static get store(): CacheStore {
|
|
42
|
+
if (!CacheManager._store) {
|
|
43
|
+
throw new ConfigurationError('CacheManager not configured. Resolve it through the container first.')
|
|
44
|
+
}
|
|
45
|
+
return CacheManager._store
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static get config(): CacheConfig {
|
|
49
|
+
return CacheManager._config
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Swap the cache store at runtime (e.g., for testing or a custom Redis store). */
|
|
53
|
+
static useStore(store: CacheStore): void {
|
|
54
|
+
CacheManager._store = store
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable cache storage backend.
|
|
3
|
+
*
|
|
4
|
+
* Implement this interface to use Redis, database, or other stores.
|
|
5
|
+
* The default in-memory implementation uses a Map with lazy TTL eviction.
|
|
6
|
+
*/
|
|
7
|
+
export interface CacheStore {
|
|
8
|
+
/** Retrieve a cached value. Returns `null` if the key doesn't exist or has expired. */
|
|
9
|
+
get<T = unknown>(key: string): Promise<T | null>
|
|
10
|
+
|
|
11
|
+
/** Store a value with optional TTL in seconds. Omit TTL for no expiry. */
|
|
12
|
+
set(key: string, value: unknown, ttl?: number): Promise<void>
|
|
13
|
+
|
|
14
|
+
/** Check if a key exists and is not expired. */
|
|
15
|
+
has(key: string): Promise<boolean>
|
|
16
|
+
|
|
17
|
+
/** Remove a single key from the cache. */
|
|
18
|
+
forget(key: string): Promise<void>
|
|
19
|
+
|
|
20
|
+
/** Remove all entries from the cache. */
|
|
21
|
+
flush(): Promise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CacheConfig {
|
|
25
|
+
/** Cache driver name. @default 'memory' */
|
|
26
|
+
default: string
|
|
27
|
+
/** Key prefix applied to all cache operations. @default '' */
|
|
28
|
+
prefix: string
|
|
29
|
+
/** Default TTL in seconds when none is specified. @default 3600 */
|
|
30
|
+
ttl: number
|
|
31
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import CacheManager from './cache_manager.ts'
|
|
2
|
+
|
|
3
|
+
function prefixed(key: string): string {
|
|
4
|
+
return CacheManager.config.prefix + key
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cache helper object — the primary API for cache-aside operations.
|
|
9
|
+
*
|
|
10
|
+
* All methods delegate to `CacheManager.store` with the configured prefix.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { cache } from '@stravigor/core/cache'
|
|
14
|
+
*
|
|
15
|
+
* const user = await cache.remember(`user:${id}`, 300, () => User.find(id))
|
|
16
|
+
* await cache.forget(`user:${id}`)
|
|
17
|
+
*/
|
|
18
|
+
export const cache = {
|
|
19
|
+
/** Retrieve a cached value. */
|
|
20
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
21
|
+
return CacheManager.store.get<T>(prefixed(key))
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/** Store a value with optional TTL in seconds. Falls back to config default. */
|
|
25
|
+
async set(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
26
|
+
return CacheManager.store.set(prefixed(key), value, ttl ?? CacheManager.config.ttl)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** Check if a key exists and is not expired. */
|
|
30
|
+
async has(key: string): Promise<boolean> {
|
|
31
|
+
return CacheManager.store.has(prefixed(key))
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/** Remove a cached value. */
|
|
35
|
+
async forget(key: string): Promise<void> {
|
|
36
|
+
return CacheManager.store.forget(prefixed(key))
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/** Clear all cached values. */
|
|
40
|
+
async flush(): Promise<void> {
|
|
41
|
+
return CacheManager.store.flush()
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cache-aside: return cached value or execute factory and cache the result.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const user = await cache.remember(`user:${id}`, 300, () => User.find(id))
|
|
49
|
+
* const stats = await cache.remember('stats', 60, async () => ({
|
|
50
|
+
* users: await User.count(),
|
|
51
|
+
* projects: await Project.count(),
|
|
52
|
+
* }))
|
|
53
|
+
*/
|
|
54
|
+
async remember<T>(key: string, ttl: number, factory: () => T | Promise<T>): Promise<T> {
|
|
55
|
+
const pk = prefixed(key)
|
|
56
|
+
const cached = await CacheManager.store.get<T>(pk)
|
|
57
|
+
if (cached !== null) return cached
|
|
58
|
+
|
|
59
|
+
const value = await factory()
|
|
60
|
+
await CacheManager.store.set(pk, value, ttl)
|
|
61
|
+
return value
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** Cache-aside with no expiry. */
|
|
65
|
+
async rememberForever<T>(key: string, factory: () => T | Promise<T>): Promise<T> {
|
|
66
|
+
const pk = prefixed(key)
|
|
67
|
+
const cached = await CacheManager.store.get<T>(pk)
|
|
68
|
+
if (cached !== null) return cached
|
|
69
|
+
|
|
70
|
+
const value = await factory()
|
|
71
|
+
await CacheManager.store.set(pk, value)
|
|
72
|
+
return value
|
|
73
|
+
},
|
|
74
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type Context from '../http/context.ts'
|
|
2
|
+
import type { Middleware } from '../http/middleware.ts'
|
|
3
|
+
|
|
4
|
+
export interface HttpCacheOptions {
|
|
5
|
+
/** Cache-Control max-age in seconds. @default 0 */
|
|
6
|
+
maxAge?: number
|
|
7
|
+
|
|
8
|
+
/** Cache-Control s-maxage for shared caches (CDN). */
|
|
9
|
+
sMaxAge?: number
|
|
10
|
+
|
|
11
|
+
/** Cache-Control directives. @default ['public'] */
|
|
12
|
+
directives?: CacheDirective[]
|
|
13
|
+
|
|
14
|
+
/** Add weak ETag header based on response body hash. @default false */
|
|
15
|
+
etag?: boolean
|
|
16
|
+
|
|
17
|
+
/** Vary header values. @default ['Accept-Encoding'] */
|
|
18
|
+
vary?: string[]
|
|
19
|
+
|
|
20
|
+
/** Skip cache headers for certain requests. */
|
|
21
|
+
skip?: (ctx: Context) => boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CacheDirective =
|
|
25
|
+
| 'public'
|
|
26
|
+
| 'private'
|
|
27
|
+
| 'no-cache'
|
|
28
|
+
| 'no-store'
|
|
29
|
+
| 'must-revalidate'
|
|
30
|
+
| 'immutable'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* HTTP cache middleware — sets Cache-Control, ETag, and Vary headers.
|
|
34
|
+
*
|
|
35
|
+
* Only applies to GET and HEAD requests. Browser/CDN does the actual caching.
|
|
36
|
+
* When `etag` is enabled and the request includes a matching `If-None-Match`
|
|
37
|
+
* header, responds with 304 Not Modified.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* router.group({ middleware: [httpCache({ maxAge: 300, etag: true })] }, r => {
|
|
41
|
+
* r.get('/api/categories', listCategories)
|
|
42
|
+
* })
|
|
43
|
+
*/
|
|
44
|
+
export function httpCache(options: HttpCacheOptions = {}): Middleware {
|
|
45
|
+
const {
|
|
46
|
+
maxAge = 0,
|
|
47
|
+
sMaxAge,
|
|
48
|
+
directives = ['public'],
|
|
49
|
+
etag: enableEtag = false,
|
|
50
|
+
vary = ['Accept-Encoding'],
|
|
51
|
+
skip,
|
|
52
|
+
} = options
|
|
53
|
+
|
|
54
|
+
const cacheControl = buildCacheControl(directives, maxAge, sMaxAge)
|
|
55
|
+
|
|
56
|
+
return async (ctx, next) => {
|
|
57
|
+
// Only cache GET and HEAD responses
|
|
58
|
+
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') return next()
|
|
59
|
+
|
|
60
|
+
if (skip?.(ctx)) return next()
|
|
61
|
+
|
|
62
|
+
const response = await next()
|
|
63
|
+
|
|
64
|
+
const headers = new Headers(response.headers)
|
|
65
|
+
headers.set('Cache-Control', cacheControl)
|
|
66
|
+
|
|
67
|
+
if (vary.length > 0) {
|
|
68
|
+
const existing = headers.get('Vary')
|
|
69
|
+
const merged = existing ? `${existing}, ${vary.join(', ')}` : vary.join(', ')
|
|
70
|
+
headers.set('Vary', merged)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (enableEtag) {
|
|
74
|
+
const body = await response.clone().arrayBuffer()
|
|
75
|
+
const hash = new Bun.CryptoHasher('md5').update(body).digest('hex')
|
|
76
|
+
const tag = `W/"${hash}"`
|
|
77
|
+
|
|
78
|
+
headers.set('ETag', tag)
|
|
79
|
+
|
|
80
|
+
const ifNoneMatch = ctx.header('if-none-match')
|
|
81
|
+
if (ifNoneMatch === tag) {
|
|
82
|
+
return new Response(null, { status: 304, headers })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Response(body, {
|
|
86
|
+
status: response.status,
|
|
87
|
+
statusText: response.statusText,
|
|
88
|
+
headers,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new Response(response.body, {
|
|
93
|
+
status: response.status,
|
|
94
|
+
statusText: response.statusText,
|
|
95
|
+
headers,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildCacheControl(
|
|
101
|
+
directives: CacheDirective[],
|
|
102
|
+
maxAge: number,
|
|
103
|
+
sMaxAge?: number
|
|
104
|
+
): string {
|
|
105
|
+
const parts = [...directives]
|
|
106
|
+
if (maxAge > 0) parts.push(`max-age=${maxAge}` as CacheDirective)
|
|
107
|
+
if (sMaxAge != null) parts.push(`s-maxage=${sMaxAge}` as CacheDirective)
|
|
108
|
+
return parts.join(', ')
|
|
109
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as CacheManager } from './cache_manager.ts'
|
|
2
|
+
export { MemoryCacheStore } from './memory_store.ts'
|
|
3
|
+
export { cache } from './helpers.ts'
|
|
4
|
+
export { httpCache } from './http_cache.ts'
|
|
5
|
+
export type { CacheStore, CacheConfig } from './cache_store.ts'
|
|
6
|
+
export type { HttpCacheOptions, CacheDirective } from './http_cache.ts'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CacheStore } from './cache_store.ts'
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
value: unknown
|
|
5
|
+
expiresAt: number | null // null = no expiry
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-memory cache store using a Map with lazy TTL eviction.
|
|
10
|
+
* Suitable for single-process deployments.
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryCacheStore implements CacheStore {
|
|
13
|
+
private entries = new Map<string, CacheEntry>()
|
|
14
|
+
|
|
15
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
16
|
+
const entry = this.entries.get(key)
|
|
17
|
+
if (!entry) return null
|
|
18
|
+
|
|
19
|
+
if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
|
|
20
|
+
this.entries.delete(key)
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return entry.value as T
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async set(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
28
|
+
const expiresAt = ttl != null ? Date.now() + ttl * 1000 : null
|
|
29
|
+
|
|
30
|
+
this.entries.set(key, { value, expiresAt })
|
|
31
|
+
|
|
32
|
+
if (this.entries.size > 10_000) this.cleanup()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async has(key: string): Promise<boolean> {
|
|
36
|
+
const entry = this.entries.get(key)
|
|
37
|
+
if (!entry) return false
|
|
38
|
+
|
|
39
|
+
if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
|
|
40
|
+
this.entries.delete(key)
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async forget(key: string): Promise<void> {
|
|
48
|
+
this.entries.delete(key)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async flush(): Promise<void> {
|
|
52
|
+
this.entries.clear()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private cleanup(): void {
|
|
56
|
+
const now = Date.now()
|
|
57
|
+
for (const [key, entry] of this.entries) {
|
|
58
|
+
if (entry.expiresAt !== null && now >= entry.expiresAt) {
|
|
59
|
+
this.entries.delete(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Configuration from '../config/configuration.ts'
|
|
2
|
+
import Database from '../database/database.ts'
|
|
3
|
+
import SchemaRegistry from '../schema/registry.ts'
|
|
4
|
+
import DatabaseIntrospector from '../database/introspector.ts'
|
|
5
|
+
|
|
6
|
+
export interface BootstrapResult {
|
|
7
|
+
config: Configuration
|
|
8
|
+
db: Database
|
|
9
|
+
registry: SchemaRegistry
|
|
10
|
+
introspector: DatabaseIntrospector
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Bootstrap the core framework services needed by CLI commands.
|
|
15
|
+
*
|
|
16
|
+
* Loads configuration, connects to the database, discovers and validates
|
|
17
|
+
* schemas, and creates an introspector instance.
|
|
18
|
+
*/
|
|
19
|
+
export async function bootstrap(): Promise<BootstrapResult> {
|
|
20
|
+
const config = new Configuration('./config')
|
|
21
|
+
await config.load()
|
|
22
|
+
|
|
23
|
+
const db = new Database(config)
|
|
24
|
+
|
|
25
|
+
const registry = new SchemaRegistry()
|
|
26
|
+
await registry.discover('database/schemas')
|
|
27
|
+
registry.validate()
|
|
28
|
+
|
|
29
|
+
const introspector = new DatabaseIntrospector(db)
|
|
30
|
+
|
|
31
|
+
return { config, db, registry, introspector }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Cleanly close the database connection. */
|
|
35
|
+
export async function shutdown(db: Database): Promise<void> {
|
|
36
|
+
await db.close()
|
|
37
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import SchemaRegistry from '../../schema/registry.ts'
|
|
5
|
+
import ApiGenerator from '../../generators/api_generator.ts'
|
|
6
|
+
import RouteGenerator from '../../generators/route_generator.ts'
|
|
7
|
+
import TestGenerator from '../../generators/test_generator.ts'
|
|
8
|
+
import DocGenerator from '../../generators/doc_generator.ts'
|
|
9
|
+
import type { ApiRoutingConfig } from '../../generators/route_generator.ts'
|
|
10
|
+
import type { GeneratorConfig } from '../../generators/config.ts'
|
|
11
|
+
|
|
12
|
+
export function registerGenerateApi(program: Command): void {
|
|
13
|
+
program
|
|
14
|
+
.command('generate:api')
|
|
15
|
+
.description(
|
|
16
|
+
'Generate services, controllers, policies, validators, events, and routes from schemas'
|
|
17
|
+
)
|
|
18
|
+
.action(async () => {
|
|
19
|
+
try {
|
|
20
|
+
console.log(chalk.cyan('Generating API layer from schemas...'))
|
|
21
|
+
|
|
22
|
+
const registry = new SchemaRegistry()
|
|
23
|
+
await registry.discover('database/schemas')
|
|
24
|
+
registry.validate()
|
|
25
|
+
|
|
26
|
+
const schemas = registry.resolve()
|
|
27
|
+
const representation = registry.buildRepresentation()
|
|
28
|
+
|
|
29
|
+
// Load generator config (if available)
|
|
30
|
+
let config: GeneratorConfig | undefined
|
|
31
|
+
try {
|
|
32
|
+
config = (await import(join(process.cwd(), 'config/generators.ts'))).default
|
|
33
|
+
} catch {
|
|
34
|
+
// No config/generators.ts — use defaults
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apiGen = new ApiGenerator(schemas, representation, config)
|
|
38
|
+
const apiFiles = await apiGen.writeAll()
|
|
39
|
+
|
|
40
|
+
// Load API routing config from config/http.ts (if available)
|
|
41
|
+
let apiConfig: Partial<ApiRoutingConfig> | undefined
|
|
42
|
+
try {
|
|
43
|
+
const httpConfig = (await import(join(process.cwd(), 'config/http.ts'))).default
|
|
44
|
+
apiConfig = httpConfig.api
|
|
45
|
+
} catch {
|
|
46
|
+
// No config/http.ts or no api section — use defaults
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const routeGen = new RouteGenerator(schemas, config, apiConfig)
|
|
50
|
+
const routeFiles = await routeGen.writeAll()
|
|
51
|
+
|
|
52
|
+
const testGen = new TestGenerator(schemas, representation, config, apiConfig)
|
|
53
|
+
const testFiles = await testGen.writeAll()
|
|
54
|
+
|
|
55
|
+
const docGen = new DocGenerator(schemas, representation, config, apiConfig)
|
|
56
|
+
const docFiles = await docGen.writeAll()
|
|
57
|
+
|
|
58
|
+
const files = [...apiFiles, ...routeFiles, ...testFiles, ...docFiles]
|
|
59
|
+
|
|
60
|
+
if (files.length === 0) {
|
|
61
|
+
console.log(chalk.yellow('No API files to generate.'))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
console.log(chalk.dim(` ${file.path}`))
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
|
|
5
|
+
export function registerGenerateKey(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('generate:key')
|
|
8
|
+
.description('Generate an APP_KEY and write it to the .env file')
|
|
9
|
+
.option('-f, --force', 'Overwrite existing APP_KEY if present')
|
|
10
|
+
.action(async ({ force }: { force?: boolean }) => {
|
|
11
|
+
try {
|
|
12
|
+
const key = crypto.randomUUID()
|
|
13
|
+
const envPath = join(process.cwd(), '.env')
|
|
14
|
+
const file = Bun.file(envPath)
|
|
15
|
+
|
|
16
|
+
if (await file.exists()) {
|
|
17
|
+
const contents = await file.text()
|
|
18
|
+
const hasKey = /^APP_KEY\s*=/m.test(contents)
|
|
19
|
+
|
|
20
|
+
if (hasKey && !force) {
|
|
21
|
+
const current = contents.match(/^APP_KEY\s*=\s*(.*)$/m)?.[1] ?? ''
|
|
22
|
+
if (current) {
|
|
23
|
+
console.log(chalk.yellow('APP_KEY already exists in .env. Use --force to overwrite.'))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (hasKey) {
|
|
29
|
+
const updated = contents.replace(/^APP_KEY\s*=.*$/m, `APP_KEY=${key}`)
|
|
30
|
+
await Bun.write(envPath, updated)
|
|
31
|
+
} else {
|
|
32
|
+
const separator = contents.endsWith('\n') ? '' : '\n'
|
|
33
|
+
await Bun.write(envPath, contents + separator + `APP_KEY=${key}\n`)
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
await Bun.write(envPath, `APP_KEY=${key}\n`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.green('APP_KEY generated successfully.'))
|
|
40
|
+
console.log(chalk.dim(` ${key}`))
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import SchemaRegistry from '../../schema/registry.ts'
|
|
5
|
+
import ModelGenerator from '../../generators/model_generator.ts'
|
|
6
|
+
import type { GeneratorConfig } from '../../generators/config.ts'
|
|
7
|
+
|
|
8
|
+
export function registerGenerateModels(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command('generate:models')
|
|
11
|
+
.description('Generate model classes and enums from schema definitions')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
try {
|
|
14
|
+
console.log(chalk.cyan('Generating models from schemas...'))
|
|
15
|
+
|
|
16
|
+
const registry = new SchemaRegistry()
|
|
17
|
+
await registry.discover('database/schemas')
|
|
18
|
+
registry.validate()
|
|
19
|
+
|
|
20
|
+
const schemas = registry.resolve()
|
|
21
|
+
const representation = registry.buildRepresentation()
|
|
22
|
+
|
|
23
|
+
// Load generator config (if available)
|
|
24
|
+
let config: GeneratorConfig | undefined
|
|
25
|
+
try {
|
|
26
|
+
config = (await import(join(process.cwd(), 'config/generators.ts'))).default
|
|
27
|
+
} catch {
|
|
28
|
+
// No config/generators.ts — use defaults
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const generator = new ModelGenerator(schemas, representation, config)
|
|
32
|
+
const files = await generator.writeAll()
|
|
33
|
+
|
|
34
|
+
if (files.length === 0) {
|
|
35
|
+
console.log(chalk.yellow('No models to generate.'))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
console.log(chalk.dim(` ${file.path}`))
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|