@alfredmouelle/create-stack 0.1.2 → 0.2.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 +42 -14
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/analytics/package.json +26 -0
- package/_stack/packages/analytics/src/adapters/noop/index.ts +12 -0
- package/_stack/packages/analytics/src/adapters/plausible/config.ts +10 -0
- package/_stack/packages/analytics/src/adapters/plausible/index.ts +94 -0
- package/_stack/packages/analytics/src/adapters/posthog/config.ts +7 -0
- package/_stack/packages/analytics/src/adapters/posthog/index.ts +50 -0
- package/_stack/packages/analytics/src/core/port.ts +30 -0
- package/_stack/packages/analytics/src/index.ts +17 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/cache/package.json +25 -0
- package/_stack/packages/cache/src/adapters/memory/index.ts +51 -0
- package/_stack/packages/cache/src/adapters/redis/config.ts +8 -0
- package/_stack/packages/cache/src/adapters/redis/index.ts +73 -0
- package/_stack/packages/cache/src/core/port.ts +29 -0
- package/_stack/packages/cache/src/core/wrap.ts +20 -0
- package/_stack/packages/cache/src/index.ts +12 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/error-tracking/package.json +25 -0
- package/_stack/packages/error-tracking/src/adapters/console/index.ts +43 -0
- package/_stack/packages/error-tracking/src/adapters/sentry/config.ts +8 -0
- package/_stack/packages/error-tracking/src/adapters/sentry/index.ts +72 -0
- package/_stack/packages/error-tracking/src/core/port.ts +39 -0
- package/_stack/packages/error-tracking/src/index.ts +14 -0
- package/_stack/packages/http/package.json +20 -0
- package/_stack/packages/http/src/api.ts +373 -0
- package/_stack/packages/http/src/index.ts +14 -0
- package/_stack/packages/http/src/responses.ts +25 -0
- package/_stack/packages/http/src/types.ts +9 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/jobs/package.json +27 -0
- package/_stack/packages/jobs/src/adapters/inngest/config.ts +8 -0
- package/_stack/packages/jobs/src/adapters/inngest/index.ts +93 -0
- package/_stack/packages/jobs/src/adapters/memory/index.ts +31 -0
- package/_stack/packages/jobs/src/adapters/trigger/config.ts +8 -0
- package/_stack/packages/jobs/src/adapters/trigger/index.ts +85 -0
- package/_stack/packages/jobs/src/core/port.ts +37 -0
- package/_stack/packages/jobs/src/index.ts +23 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/logger/package.json +25 -0
- package/_stack/packages/logger/src/adapters/console/config.ts +7 -0
- package/_stack/packages/logger/src/adapters/console/index.ts +69 -0
- package/_stack/packages/logger/src/adapters/pino/index.ts +54 -0
- package/_stack/packages/logger/src/core/port.ts +21 -0
- package/_stack/packages/logger/src/index.ts +12 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/packages/storage/package.json +27 -0
- package/_stack/packages/storage/src/adapters/gcs/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/gcs/index.ts +111 -0
- package/_stack/packages/storage/src/adapters/local/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/local/index.ts +78 -0
- package/_stack/packages/storage/src/adapters/r2/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/r2/index.ts +39 -0
- package/_stack/packages/storage/src/adapters/s3/config.ts +11 -0
- package/_stack/packages/storage/src/adapters/s3/index.ts +143 -0
- package/_stack/packages/storage/src/core/port.ts +41 -0
- package/_stack/packages/storage/src/index.ts +21 -0
- package/index.mjs +69 -18
- package/lib/build.mjs +23 -5
- package/lib/capabilities.mjs +375 -0
- package/lib/env.mjs +21 -0
- package/lib/scaffold.mjs +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/jobs",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@alfredmouelle/http": "workspace:*",
|
|
17
|
+
"@trigger.dev/sdk": "^4.4.6",
|
|
18
|
+
"inngest": "^4.7.0",
|
|
19
|
+
"valibot": "^1.4.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.10.2",
|
|
23
|
+
"tsdown": "^0.22.3",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "^4.1.9"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { FetchHandler } from '@alfredmouelle/http'
|
|
2
|
+
import { Inngest } from 'inngest'
|
|
3
|
+
import * as v from 'valibot'
|
|
4
|
+
import type { JobDefinition, JobEvent, JobsPort } from '../../core/port.js'
|
|
5
|
+
import { JobsError } from '../../core/port.js'
|
|
6
|
+
import { InngestConfigSchema } from './config.js'
|
|
7
|
+
|
|
8
|
+
/** Opaque Inngest function from `createFunction`. */
|
|
9
|
+
export type InngestFunction = unknown
|
|
10
|
+
|
|
11
|
+
/** Structural view of the Inngest client; real `Inngest` satisfies it, tests mock it. */
|
|
12
|
+
export interface InngestLike {
|
|
13
|
+
send(payload: { name: string; data: unknown }): Promise<unknown>
|
|
14
|
+
createFunction(
|
|
15
|
+
config: { id: string },
|
|
16
|
+
trigger: { event: string },
|
|
17
|
+
handler: (ctx: { event: { name: string; data: unknown } }) => Promise<void> | void,
|
|
18
|
+
): InngestFunction
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InngestAdapterOptions {
|
|
22
|
+
/** Inngest app id (`new Inngest({ id })`). */
|
|
23
|
+
id: string
|
|
24
|
+
/** Inject custom/mock client. Defaults to real `Inngest`. */
|
|
25
|
+
client?: InngestLike
|
|
26
|
+
/** Event key for sending in production. */
|
|
27
|
+
eventKey?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InngestJobsAdapter extends JobsPort {
|
|
31
|
+
/** Functions from `defineJob`, for Inngest's `serve`. */
|
|
32
|
+
readonly functions: InngestFunction[]
|
|
33
|
+
/** Underlying client for advanced use (steps, cron, concurrency, …). */
|
|
34
|
+
readonly client: InngestLike
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inngestAdapter(options: InngestAdapterOptions): InngestJobsAdapter {
|
|
38
|
+
// Validate early: bad id fails at construction, not trigger().
|
|
39
|
+
const config = v.parse(InngestConfigSchema, { id: options.id, eventKey: options.eventKey })
|
|
40
|
+
const client: InngestLike =
|
|
41
|
+
options.client ??
|
|
42
|
+
(new Inngest({ id: config.id, eventKey: config.eventKey }) as unknown as InngestLike)
|
|
43
|
+
|
|
44
|
+
const functions: InngestFunction[] = []
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: 'inngest',
|
|
48
|
+
functions,
|
|
49
|
+
client,
|
|
50
|
+
defineJob<T>(def: JobDefinition<T>): JobDefinition<T> {
|
|
51
|
+
const fn = client.createFunction({ id: def.id }, { event: def.event }, ({ event }) =>
|
|
52
|
+
def.handler(event as JobEvent<T>),
|
|
53
|
+
)
|
|
54
|
+
functions.push(fn)
|
|
55
|
+
return def
|
|
56
|
+
},
|
|
57
|
+
async trigger<T>(event: JobEvent<T>): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await client.send({ name: event.name, data: event.data })
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new JobsError('Failed to send event to Inngest', { adapter: 'inngest', cause })
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Collect functions for `serve({ client, functions })`. */
|
|
68
|
+
export function inngestFunctions(adapter: InngestJobsAdapter): InngestFunction[] {
|
|
69
|
+
return adapter.functions
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Inngest's framework-agnostic `serve` (e.g. `inngest/edge`). Loosely typed so
|
|
74
|
+
* the real `serve` (richer options than our `InngestLike`) is accepted.
|
|
75
|
+
*/
|
|
76
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts the real serve whose options are richer than InngestLike
|
|
77
|
+
export type InngestServe = (options: any) => FetchHandler
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* {@link FetchHandler} serving the adapter's functions. Pass Inngest's `serve`
|
|
81
|
+
* (from `inngest/edge`); the handler mounts in Next.js or TanStack Start unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export function inngestServeHandler<TServe extends InngestServe>(
|
|
84
|
+
adapter: InngestJobsAdapter,
|
|
85
|
+
serve: TServe,
|
|
86
|
+
options: { signingKey?: string } = {},
|
|
87
|
+
): FetchHandler {
|
|
88
|
+
return serve({
|
|
89
|
+
client: adapter.client,
|
|
90
|
+
functions: adapter.functions,
|
|
91
|
+
signingKey: options.signingKey,
|
|
92
|
+
} as Parameters<TServe>[0])
|
|
93
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JobDefinition, JobEvent, JobsPort } from '../../core/port.js'
|
|
2
|
+
|
|
3
|
+
export interface MemoryJobsAdapter extends JobsPort {
|
|
4
|
+
/** Registered jobs, keyed by subscribed event name. */
|
|
5
|
+
readonly jobs: ReadonlyMap<string, JobDefinition[]>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-process jobs adapter for dev/tests. `trigger` runs every matching handler
|
|
10
|
+
* inline (awaiting async) — no queue, no network. Unit-test jobs without Inngest.
|
|
11
|
+
*/
|
|
12
|
+
export function memoryAdapter(): MemoryJobsAdapter {
|
|
13
|
+
const jobs = new Map<string, JobDefinition[]>()
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: 'memory',
|
|
17
|
+
jobs,
|
|
18
|
+
defineJob<T>(def: JobDefinition<T>): JobDefinition<T> {
|
|
19
|
+
const existing = jobs.get(def.event) ?? []
|
|
20
|
+
existing.push(def as JobDefinition)
|
|
21
|
+
jobs.set(def.event, existing)
|
|
22
|
+
return def
|
|
23
|
+
},
|
|
24
|
+
async trigger<T>(event: JobEvent<T>): Promise<void> {
|
|
25
|
+
const handlers = jobs.get(event.name) ?? []
|
|
26
|
+
for (const def of handlers) {
|
|
27
|
+
await def.handler(event)
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
|
|
3
|
+
export const TriggerDevConfigSchema = v.object({
|
|
4
|
+
/** Trigger auth key. Falls back to `TRIGGER_SECRET_KEY`. */
|
|
5
|
+
secretKey: v.optional(v.string()),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export type TriggerDevConfig = v.InferOutput<typeof TriggerDevConfigSchema>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { configure, task, tasks } from '@trigger.dev/sdk'
|
|
2
|
+
import * as v from 'valibot'
|
|
3
|
+
import type { JobDefinition, JobEvent, JobsPort } from '../../core/port.js'
|
|
4
|
+
import { JobsError } from '../../core/port.js'
|
|
5
|
+
import { TriggerDevConfigSchema } from './config.js'
|
|
6
|
+
|
|
7
|
+
/** Opaque Trigger.dev task from `task()`. */
|
|
8
|
+
export type TriggerTask = unknown
|
|
9
|
+
|
|
10
|
+
/** Structural view of the Trigger.dev SDK; real `task`/`tasks.trigger` satisfy it, tests mock it. */
|
|
11
|
+
export interface TriggerDevLike {
|
|
12
|
+
task(config: { id: string; run: (payload: unknown) => Promise<void> | void }): TriggerTask
|
|
13
|
+
trigger(id: string, payload: unknown): Promise<unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TriggerDevAdapterOptions {
|
|
17
|
+
/** Trigger auth key. Defaults to `TRIGGER_SECRET_KEY`. */
|
|
18
|
+
secretKey?: string
|
|
19
|
+
/** Inject custom/mock SDK. Defaults to real `@trigger.dev/sdk`. */
|
|
20
|
+
client?: TriggerDevLike
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TriggerDevJobsAdapter extends JobsPort {
|
|
24
|
+
/** Tasks from `defineJob`, to re-export from your Trigger.dev `dirs`. */
|
|
25
|
+
readonly tasks: TriggerTask[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Bridge real SDK to {@link TriggerDevLike}; richer signatures narrowed here. */
|
|
29
|
+
function defaultClient(secretKey?: string): TriggerDevLike {
|
|
30
|
+
if (secretKey) configure({ secretKey })
|
|
31
|
+
return {
|
|
32
|
+
task: task as unknown as TriggerDevLike['task'],
|
|
33
|
+
trigger: (id, payload) =>
|
|
34
|
+
(tasks.trigger as (id: string, payload: unknown) => Promise<unknown>)(id, payload),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Trigger.dev jobs adapter. Each {@link JobDefinition} becomes a task; `trigger`
|
|
40
|
+
* routes an event to every task subscribed to its `name`.
|
|
41
|
+
*
|
|
42
|
+
* No event bus (tasks trigger by id), so the adapter keeps an event → task-ids
|
|
43
|
+
* map and fans out on `trigger`. Created tasks collect on `adapter.tasks` for
|
|
44
|
+
* re-export from a file under your `dirs` (how the CLI discovers them).
|
|
45
|
+
*/
|
|
46
|
+
export function triggerDevAdapter(options: TriggerDevAdapterOptions = {}): TriggerDevJobsAdapter {
|
|
47
|
+
const config = v.parse(TriggerDevConfigSchema, { secretKey: options.secretKey })
|
|
48
|
+
const client: TriggerDevLike = options.client ?? defaultClient(config.secretKey)
|
|
49
|
+
|
|
50
|
+
const createdTasks: TriggerTask[] = []
|
|
51
|
+
// event name -> subscribed task ids
|
|
52
|
+
const routes = new Map<string, string[]>()
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: 'trigger.dev',
|
|
56
|
+
tasks: createdTasks,
|
|
57
|
+
defineJob<T>(def: JobDefinition<T>): JobDefinition<T> {
|
|
58
|
+
const created = client.task({
|
|
59
|
+
id: def.id,
|
|
60
|
+
run: (payload) => def.handler({ name: def.event, data: payload } as JobEvent<T>),
|
|
61
|
+
})
|
|
62
|
+
createdTasks.push(created)
|
|
63
|
+
const ids = routes.get(def.event) ?? []
|
|
64
|
+
ids.push(def.id)
|
|
65
|
+
routes.set(def.event, ids)
|
|
66
|
+
return def
|
|
67
|
+
},
|
|
68
|
+
async trigger<T>(event: JobEvent<T>): Promise<void> {
|
|
69
|
+
const ids = routes.get(event.name) ?? []
|
|
70
|
+
try {
|
|
71
|
+
await Promise.all(ids.map((id) => client.trigger(id, event.data)))
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
throw new JobsError('Failed to trigger Trigger.dev task', {
|
|
74
|
+
adapter: 'trigger.dev',
|
|
75
|
+
cause,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Collect tasks to re-export from a file under your `dirs`. */
|
|
83
|
+
export function triggerDevTasks(adapter: TriggerDevJobsAdapter): TriggerTask[] {
|
|
84
|
+
return adapter.tasks
|
|
85
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Event through the jobs system. `name` routes to subscribed jobs; `data` is the serializable payload. */
|
|
2
|
+
export interface JobEvent<T = unknown> {
|
|
3
|
+
name: string
|
|
4
|
+
data: T
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Background work unit. Subscribes to one `event` name; `handler` runs on a matching event. */
|
|
8
|
+
export interface JobDefinition<T = unknown> {
|
|
9
|
+
id: string
|
|
10
|
+
event: string
|
|
11
|
+
handler: (event: JobEvent<T>) => Promise<void> | void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The port the app depends on. Swap provider = swap adapter; this never changes.
|
|
16
|
+
*
|
|
17
|
+
* Minimal and event-driven. Rich provider features (steps, concurrency, cron,
|
|
18
|
+
* fan-out) are NOT modeled — use the underlying SDK directly. See the README.
|
|
19
|
+
*/
|
|
20
|
+
export interface JobsPort {
|
|
21
|
+
readonly name: string
|
|
22
|
+
/** Register a job. Returns the definition for collection/export. */
|
|
23
|
+
defineJob<T>(def: JobDefinition<T>): JobDefinition<T>
|
|
24
|
+
/** Emit an event, triggering every job subscribed to `event.name`. */
|
|
25
|
+
trigger<T>(event: JobEvent<T>): Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Normalized adapter error so callers never catch provider types. */
|
|
29
|
+
export class JobsError extends Error {
|
|
30
|
+
readonly adapter: string
|
|
31
|
+
|
|
32
|
+
constructor(message: string, options: { adapter: string; cause?: unknown }) {
|
|
33
|
+
super(message, { cause: options.cause })
|
|
34
|
+
this.name = 'JobsError'
|
|
35
|
+
this.adapter = options.adapter
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { type InngestConfig, InngestConfigSchema } from './adapters/inngest/config.js'
|
|
2
|
+
export {
|
|
3
|
+
type InngestAdapterOptions,
|
|
4
|
+
type InngestFunction,
|
|
5
|
+
type InngestJobsAdapter,
|
|
6
|
+
type InngestLike,
|
|
7
|
+
type InngestServe,
|
|
8
|
+
inngestAdapter,
|
|
9
|
+
inngestFunctions,
|
|
10
|
+
inngestServeHandler,
|
|
11
|
+
} from './adapters/inngest/index.js'
|
|
12
|
+
export { type MemoryJobsAdapter, memoryAdapter } from './adapters/memory/index.js'
|
|
13
|
+
export { type TriggerDevConfig, TriggerDevConfigSchema } from './adapters/trigger/config.js'
|
|
14
|
+
export {
|
|
15
|
+
type TriggerDevAdapterOptions,
|
|
16
|
+
type TriggerDevJobsAdapter,
|
|
17
|
+
type TriggerDevLike,
|
|
18
|
+
type TriggerTask,
|
|
19
|
+
triggerDevAdapter,
|
|
20
|
+
triggerDevTasks,
|
|
21
|
+
} from './adapters/trigger/index.js'
|
|
22
|
+
export type { JobDefinition, JobEvent, JobsPort } from './core/port.js'
|
|
23
|
+
export { JobsError } from './core/port.js'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "logger",
|
|
4
|
+
"description": "Structured logging behind a swappable port. Application code depends only on the Logger interface; pick an adapter (pino, console) at the composition root.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"defaultAdapter": "pino",
|
|
7
|
+
"adapters": {
|
|
8
|
+
"pino": {
|
|
9
|
+
"deps": ["pino"],
|
|
10
|
+
"env": [],
|
|
11
|
+
"files": ["src/adapters/pino"]
|
|
12
|
+
},
|
|
13
|
+
"console": {
|
|
14
|
+
"deps": [],
|
|
15
|
+
"env": [],
|
|
16
|
+
"files": ["src/adapters/console"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"sharedDeps": ["valibot"],
|
|
20
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/logger",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"pino": "^10.3.1",
|
|
17
|
+
"valibot": "^1.4.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.2",
|
|
21
|
+
"tsdown": "^0.22.3",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^4.1.9"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
import type { LogFields, Logger, LogLevel } from '../../core/port.js'
|
|
3
|
+
import { ConsoleConfigSchema } from './config.js'
|
|
4
|
+
|
|
5
|
+
export interface ConsoleAdapterOptions {
|
|
6
|
+
/** Min level to emit; lower-severity dropped. Defaults to `'info'`. */
|
|
7
|
+
level?: LogLevel
|
|
8
|
+
/** Fields pinned onto every line. */
|
|
9
|
+
bindings?: LogFields
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ORDER: Record<LogLevel, number> = {
|
|
13
|
+
trace: 10,
|
|
14
|
+
debug: 20,
|
|
15
|
+
info: 30,
|
|
16
|
+
warn: 40,
|
|
17
|
+
error: 50,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SINK: Record<LogLevel, (...args: unknown[]) => void> = {
|
|
21
|
+
// biome-ignore lint/suspicious/noConsole: console adapter
|
|
22
|
+
trace: (...args) => console.debug(...args),
|
|
23
|
+
// biome-ignore lint/suspicious/noConsole: console adapter
|
|
24
|
+
debug: (...args) => console.debug(...args),
|
|
25
|
+
// biome-ignore lint/suspicious/noConsole: console adapter
|
|
26
|
+
info: (...args) => console.info(...args),
|
|
27
|
+
warn: (...args) => console.warn(...args),
|
|
28
|
+
error: (...args) => console.error(...args),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function format(level: LogLevel, msg: string, fields: LogFields): string {
|
|
32
|
+
const hasFields = Object.keys(fields).length > 0
|
|
33
|
+
return hasFields ? `[${level}] ${msg} ${JSON.stringify(fields)}` : `[${level}] ${msg}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function build(min: number, bindings: LogFields): Logger {
|
|
37
|
+
const log = (level: LogLevel, msg: string, fields: LogFields = {}) => {
|
|
38
|
+
if (ORDER[level] < min) return
|
|
39
|
+
const merged = { ...bindings, ...fields }
|
|
40
|
+
SINK[level](format(level, msg, merged))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name: 'console',
|
|
45
|
+
trace(msg, fields) {
|
|
46
|
+
log('trace', msg, fields)
|
|
47
|
+
},
|
|
48
|
+
debug(msg, fields) {
|
|
49
|
+
log('debug', msg, fields)
|
|
50
|
+
},
|
|
51
|
+
info(msg, fields) {
|
|
52
|
+
log('info', msg, fields)
|
|
53
|
+
},
|
|
54
|
+
warn(msg, fields) {
|
|
55
|
+
log('warn', msg, fields)
|
|
56
|
+
},
|
|
57
|
+
error(msg, fields) {
|
|
58
|
+
log('error', msg, fields)
|
|
59
|
+
},
|
|
60
|
+
child(childBindings) {
|
|
61
|
+
return build(min, { ...bindings, ...childBindings })
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function consoleAdapter(options: ConsoleAdapterOptions = {}): Logger {
|
|
67
|
+
const config = v.parse(ConsoleConfigSchema, { level: options.level })
|
|
68
|
+
return build(ORDER[config.level], options.bindings ?? {})
|
|
69
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import pino from 'pino'
|
|
2
|
+
import type { LogFields, Logger, LogLevel } from '../../core/port.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal structural view of a pino logger (eases testing).
|
|
6
|
+
* Level methods follow pino's `(mergingObject, message)` convention.
|
|
7
|
+
*/
|
|
8
|
+
export interface PinoLike {
|
|
9
|
+
trace(obj: LogFields, msg: string): void
|
|
10
|
+
debug(obj: LogFields, msg: string): void
|
|
11
|
+
info(obj: LogFields, msg: string): void
|
|
12
|
+
warn(obj: LogFields, msg: string): void
|
|
13
|
+
error(obj: LogFields, msg: string): void
|
|
14
|
+
child(bindings: LogFields): PinoLike
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PinoAdapterOptions {
|
|
18
|
+
/** Min level to emit; defaults to `'info'`. */
|
|
19
|
+
level?: LogLevel
|
|
20
|
+
/** Fields pinned onto every line. */
|
|
21
|
+
bindings?: LogFields
|
|
22
|
+
/** Inject a custom/mock pino; defaults to a real `pino()`. */
|
|
23
|
+
client?: PinoLike
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrap(client: PinoLike): Logger {
|
|
27
|
+
return {
|
|
28
|
+
name: 'pino',
|
|
29
|
+
trace(msg, fields = {}) {
|
|
30
|
+
client.trace(fields, msg)
|
|
31
|
+
},
|
|
32
|
+
debug(msg, fields = {}) {
|
|
33
|
+
client.debug(fields, msg)
|
|
34
|
+
},
|
|
35
|
+
info(msg, fields = {}) {
|
|
36
|
+
client.info(fields, msg)
|
|
37
|
+
},
|
|
38
|
+
warn(msg, fields = {}) {
|
|
39
|
+
client.warn(fields, msg)
|
|
40
|
+
},
|
|
41
|
+
error(msg, fields = {}) {
|
|
42
|
+
client.error(fields, msg)
|
|
43
|
+
},
|
|
44
|
+
child(bindings) {
|
|
45
|
+
return wrap(client.child(bindings))
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function pinoAdapter(options: PinoAdapterOptions = {}): Logger {
|
|
51
|
+
const base = options.client ?? (pino({ level: options.level ?? 'info' }) as unknown as PinoLike)
|
|
52
|
+
const client = options.bindings ? base.child(options.bindings) : base
|
|
53
|
+
return wrap(client)
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Severity levels, from most to least verbose. */
|
|
2
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'
|
|
3
|
+
|
|
4
|
+
/** Arbitrary structured context attached to a log line. */
|
|
5
|
+
export type LogFields = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* App-facing port; swap adapters at the composition root, never this interface.
|
|
9
|
+
* Each method takes a message + optional fields; `child` merges `bindings` into every line.
|
|
10
|
+
*/
|
|
11
|
+
export interface Logger {
|
|
12
|
+
/** Adapter name (`'pino'`, `'console'`, …). */
|
|
13
|
+
readonly name: string
|
|
14
|
+
trace(msg: string, fields?: LogFields): void
|
|
15
|
+
debug(msg: string, fields?: LogFields): void
|
|
16
|
+
info(msg: string, fields?: LogFields): void
|
|
17
|
+
warn(msg: string, fields?: LogFields): void
|
|
18
|
+
error(msg: string, fields?: LogFields): void
|
|
19
|
+
/** Derive a logger pinning `bindings` onto subsequent lines. */
|
|
20
|
+
child(bindings: LogFields): Logger
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { type ConsoleConfig, ConsoleConfigSchema } from './adapters/console/config.js'
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
type ConsoleAdapterOptions,
|
|
5
|
+
consoleAdapter,
|
|
6
|
+
} from './adapters/console/index.js'
|
|
7
|
+
export {
|
|
8
|
+
type PinoAdapterOptions,
|
|
9
|
+
type PinoLike,
|
|
10
|
+
pinoAdapter,
|
|
11
|
+
} from './adapters/pino/index.js'
|
|
12
|
+
export type { LogFields, Logger, LogLevel } from './core/port.js'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../capability.schema.json",
|
|
3
|
+
"name": "storage",
|
|
4
|
+
"description": "Object storage behind a swappable port: put/get/delete/exists plus signed URLs. Adapters for S3, Cloudflare R2, Google Cloud Storage and the local filesystem.",
|
|
5
|
+
"port": "src/core/port.ts",
|
|
6
|
+
"factory": "src/index.ts",
|
|
7
|
+
"defaultAdapter": "s3",
|
|
8
|
+
"adapters": {
|
|
9
|
+
"s3": {
|
|
10
|
+
"deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
|
|
11
|
+
"env": ["S3_BUCKET", "S3_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
12
|
+
"files": ["src/adapters/s3"]
|
|
13
|
+
},
|
|
14
|
+
"r2": {
|
|
15
|
+
"deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
|
|
16
|
+
"env": ["R2_BUCKET", "R2_ACCOUNT_ID", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"],
|
|
17
|
+
"files": ["src/adapters/r2", "src/adapters/s3"]
|
|
18
|
+
},
|
|
19
|
+
"gcs": {
|
|
20
|
+
"deps": ["@google-cloud/storage"],
|
|
21
|
+
"env": ["GCS_BUCKET", "GOOGLE_CLOUD_PROJECT"],
|
|
22
|
+
"files": ["src/adapters/gcs"]
|
|
23
|
+
},
|
|
24
|
+
"local": {
|
|
25
|
+
"deps": [],
|
|
26
|
+
"env": ["STORAGE_LOCAL_DIR"],
|
|
27
|
+
"files": ["src/adapters/local"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"sharedDeps": ["valibot"],
|
|
31
|
+
"sharedFiles": ["src/core", "src/index.ts"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/storage",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@aws-sdk/client-s3": "^3.1073.0",
|
|
17
|
+
"@aws-sdk/s3-request-presigner": "^3.1073.0",
|
|
18
|
+
"@google-cloud/storage": "^7.21.0",
|
|
19
|
+
"valibot": "^1.4.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.10.2",
|
|
23
|
+
"tsdown": "^0.22.3",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "^4.1.9"
|
|
26
|
+
}
|
|
27
|
+
}
|