@silkweave/trpc 1.7.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/.turbo/turbo-build.log +16 -0
- package/.turbo/turbo-check.log +13 -0
- package/.turbo/turbo-prepack.log +24 -0
- package/README.md +184 -0
- package/build/index.d.mts +91 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.mjs +267 -0
- package/build/index.mjs.map +1 -0
- package/eslint.config.mjs +93 -0
- package/package.json +45 -0
- package/src/adapter/fetch.ts +112 -0
- package/src/adapter/trpc.ts +105 -0
- package/src/index.ts +6 -0
- package/src/lib/buildRouter.ts +27 -0
- package/src/lib/createContext.ts +44 -0
- package/src/lib/errors.ts +42 -0
- package/src/lib/inferRouter.ts +50 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +7 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import eslint from '@eslint/js'
|
|
2
|
+
import stylistic from '@stylistic/eslint-plugin'
|
|
3
|
+
import { defineConfig } from 'eslint/config'
|
|
4
|
+
import tseslint from 'typescript-eslint'
|
|
5
|
+
|
|
6
|
+
export default defineConfig(eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, {
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parserOptions: {
|
|
9
|
+
project: 'tsconfig.json',
|
|
10
|
+
tsconfigRootDir: import.meta.dirname
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
plugins: { '@stylistic': stylistic },
|
|
14
|
+
rules: {
|
|
15
|
+
'@stylistic/space-in-parens': ['error'],
|
|
16
|
+
'@stylistic/comma-spacing': ['error'],
|
|
17
|
+
'@stylistic/no-multi-spaces': ['error'],
|
|
18
|
+
'@stylistic/no-trailing-spaces': ['error'],
|
|
19
|
+
'@stylistic/no-whitespace-before-property': ['error'],
|
|
20
|
+
'@stylistic/array-bracket-newline': ['error', 'consistent'],
|
|
21
|
+
'@stylistic/array-bracket-spacing': ['error'],
|
|
22
|
+
'@stylistic/arrow-spacing': ['error'],
|
|
23
|
+
'@stylistic/arrow-parens': ['error', 'always'],
|
|
24
|
+
'@stylistic/block-spacing': ['error', 'always'],
|
|
25
|
+
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
|
26
|
+
'@stylistic/comma-dangle': ['error', 'never'],
|
|
27
|
+
'@stylistic/key-spacing': ['error'],
|
|
28
|
+
'@stylistic/keyword-spacing': ['error'],
|
|
29
|
+
'@stylistic/member-delimiter-style': ['error', { 'multiline': { 'delimiter': 'none' } }],
|
|
30
|
+
'@stylistic/no-extra-semi': ['error'],
|
|
31
|
+
'@stylistic/indent': ['error', 2],
|
|
32
|
+
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1, 'maxEOF': 0, 'maxBOF': 0 }],
|
|
33
|
+
'@stylistic/object-curly-spacing': ['error', 'always'],
|
|
34
|
+
'@stylistic/quotes': ['error', 'single'],
|
|
35
|
+
'@stylistic/semi': ['error', 'never'],
|
|
36
|
+
'@stylistic/space-before-blocks': ['error', 'always'],
|
|
37
|
+
'@stylistic/space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }],
|
|
38
|
+
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
|
39
|
+
'@typescript-eslint/array-type': 'off',
|
|
40
|
+
'@typescript-eslint/await-thenable': 'error',
|
|
41
|
+
'@typescript-eslint/ban-types': 'off',
|
|
42
|
+
'@typescript-eslint/consistent-type-assertions': 'off',
|
|
43
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
44
|
+
'@typescript-eslint/explicit-member-accessibility': 'off',
|
|
45
|
+
'@typescript-eslint/no-angle-bracket-type-assertion': 'off',
|
|
46
|
+
'@typescript-eslint/no-empty-function': 'off',
|
|
47
|
+
'@typescript-eslint/no-empty-interface': 'off',
|
|
48
|
+
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
|
49
|
+
'@typescript-eslint/no-floating-promises': 'off',
|
|
50
|
+
'@typescript-eslint/no-misused-new': 'error',
|
|
51
|
+
'@typescript-eslint/no-misused-promises': 'off',
|
|
52
|
+
'@typescript-eslint/no-namespace': 'off',
|
|
53
|
+
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
|
54
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
55
|
+
'@typescript-eslint/no-object-literal-type-assertion': 'off',
|
|
56
|
+
'@typescript-eslint/no-parameter-properties': 'off',
|
|
57
|
+
'@typescript-eslint/no-shadow': 'error',
|
|
58
|
+
'@typescript-eslint/no-triple-slash-reference': 'off',
|
|
59
|
+
'@typescript-eslint/no-unused-vars': ['error', {
|
|
60
|
+
'args': 'all',
|
|
61
|
+
'argsIgnorePattern': '^_',
|
|
62
|
+
'caughtErrors': 'all',
|
|
63
|
+
'caughtErrorsIgnorePattern': '^_',
|
|
64
|
+
'destructuredArrayIgnorePattern': '^_',
|
|
65
|
+
'varsIgnorePattern': '^_',
|
|
66
|
+
'ignoreRestSiblings': true
|
|
67
|
+
}],
|
|
68
|
+
'@typescript-eslint/no-use-before-define': 'off',
|
|
69
|
+
'@typescript-eslint/no-var-requires': 'off',
|
|
70
|
+
'@typescript-eslint/prefer-for-of': 'error',
|
|
71
|
+
'@typescript-eslint/prefer-interface': 'off',
|
|
72
|
+
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
|
73
|
+
'@typescript-eslint/prefer-optional-chain': 'error',
|
|
74
|
+
'@typescript-eslint/return-await': 'error',
|
|
75
|
+
'@typescript-eslint/unified-signatures': 'error',
|
|
76
|
+
'@typescript-eslint/no-unsafe-return': 'off',
|
|
77
|
+
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
|
78
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
79
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
80
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
81
|
+
'@typescript-eslint/dot-notation': 'off',
|
|
82
|
+
'@typescript-eslint/prefer-regexp-exec': 'off',
|
|
83
|
+
'@typescript-eslint/require-await': 'off',
|
|
84
|
+
'@typescript-eslint/only-throw-error': 'error',
|
|
85
|
+
'@typescript-eslint/no-base-to-string': 'off',
|
|
86
|
+
'@typescript-eslint/restrict-template-expressions': 'off'
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
files: ['eslint.config.mjs'],
|
|
90
|
+
extends: [tseslint.configs.disableTypeChecked]
|
|
91
|
+
}, {
|
|
92
|
+
ignores: ['build', 'tsdown.config.ts', 'scratch']
|
|
93
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silkweave/trpc",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Silkweave tRPC Adapter",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+ssh://git@github.com/silkweave/silkweave.git",
|
|
8
|
+
"directory": "packages/trpc"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "build/index.mjs",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@trpc/server": "^11.7.1",
|
|
14
|
+
"change-case": "^5.4.4",
|
|
15
|
+
"cors": "^2.8.5",
|
|
16
|
+
"zod": "^4.3.6",
|
|
17
|
+
"@silkweave/auth": "1.7.0",
|
|
18
|
+
"@silkweave/core": "1.7.0",
|
|
19
|
+
"@silkweave/logger": "1.7.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
23
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
24
|
+
"@trpc/client": "^11.7.1",
|
|
25
|
+
"@types/cors": "^2.8.19",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"rimraf": "^6.1.3",
|
|
28
|
+
"tsdown": "^0.21.8",
|
|
29
|
+
"tsx": "^4.21.0",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"typescript-eslint": "^8.56.1"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"clean": "rimraf build",
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"watch": "tsdown --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"lint": "eslint",
|
|
39
|
+
"check": "pnpm lint && pnpm typecheck"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./build/index.mjs"
|
|
43
|
+
},
|
|
44
|
+
"types": "./build/index.d.ts"
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AuthConfig } from '@silkweave/auth'
|
|
2
|
+
import { Adapter, AdapterGenerator, SilkweaveOptions } from '@silkweave/core'
|
|
3
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
4
|
+
import { buildRouter, TrpcHandlerContext } from '../lib/buildRouter.js'
|
|
5
|
+
import { createActionLogger, resolveAuth } from '../lib/createContext.js'
|
|
6
|
+
|
|
7
|
+
export interface TrpcFetchAdapterOptions {
|
|
8
|
+
/** URL prefix stripped from incoming requests before tRPC routing. Default `/trpc`. */
|
|
9
|
+
endpoint?: string
|
|
10
|
+
auth?: AuthConfig
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FetchHandler = (request: Request) => Promise<Response>
|
|
14
|
+
|
|
15
|
+
export interface TrpcFetchAdapter {
|
|
16
|
+
adapter: AdapterGenerator
|
|
17
|
+
handler: FetchHandler
|
|
18
|
+
GET: FetchHandler
|
|
19
|
+
POST: FetchHandler
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a tRPC adapter that exposes a fetch-compatible handler instead of
|
|
24
|
+
* binding its own HTTP server. Use in Astro API routes, Vercel serverless
|
|
25
|
+
* functions, Cloudflare Workers, or any Web Standard runtime.
|
|
26
|
+
*
|
|
27
|
+
* The returned `handler` waits for `server.start()` to complete (via an internal
|
|
28
|
+
* `_ready` promise) before dispatching requests, so it's safe to call from a
|
|
29
|
+
* cold-started serverless invocation.
|
|
30
|
+
*
|
|
31
|
+
* CORS is intentionally not configured here - handle it in your host framework
|
|
32
|
+
* (Astro middleware, `vercel.json` headers, Worker response headers, etc).
|
|
33
|
+
*/
|
|
34
|
+
export function trpcFetch(options: TrpcFetchAdapterOptions = {}): TrpcFetchAdapter {
|
|
35
|
+
const endpoint = (options.endpoint ?? '/trpc').replace(/\/$/, '')
|
|
36
|
+
|
|
37
|
+
let resolveReady!: () => void
|
|
38
|
+
const ready = new Promise<void>((resolve) => { resolveReady = resolve })
|
|
39
|
+
|
|
40
|
+
let handler: FetchHandler = async () => new Response(
|
|
41
|
+
JSON.stringify({ error: 'not_ready', message: 'tRPC adapter has not started yet' }),
|
|
42
|
+
{ status: 503, headers: { 'Content-Type': 'application/json' } }
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const adapter: AdapterGenerator = (_silkweaveOptions: SilkweaveOptions, baseContext): Adapter => {
|
|
46
|
+
const context = baseContext.fork({ adapter: 'trpc' })
|
|
47
|
+
return {
|
|
48
|
+
context,
|
|
49
|
+
start: async (actions) => {
|
|
50
|
+
const router = buildRouter(actions)
|
|
51
|
+
const logger = createActionLogger()
|
|
52
|
+
|
|
53
|
+
const createContext = async (
|
|
54
|
+
opts: { req: Request }
|
|
55
|
+
): Promise<TrpcHandlerContext> => {
|
|
56
|
+
const resolved = await resolveAuth(
|
|
57
|
+
options.auth,
|
|
58
|
+
opts.req.headers.get('authorization'),
|
|
59
|
+
context.fork({ request: opts.req })
|
|
60
|
+
)
|
|
61
|
+
if (resolved.kind === 'error') {
|
|
62
|
+
const err = new Error('Unauthorized') as Error & { silkweaveAuthError?: typeof resolved.error }
|
|
63
|
+
err.silkweaveAuthError = resolved.error
|
|
64
|
+
throw err
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
silkweaveContext: context.fork({
|
|
68
|
+
logger,
|
|
69
|
+
request: opts.req,
|
|
70
|
+
...(resolved.authInfo ? { auth: resolved.authInfo } : {})
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
handler = async (request: Request): Promise<Response> => {
|
|
76
|
+
try {
|
|
77
|
+
return await fetchRequestHandler({
|
|
78
|
+
endpoint,
|
|
79
|
+
req: request,
|
|
80
|
+
router,
|
|
81
|
+
createContext
|
|
82
|
+
})
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const authError = (error as { silkweaveAuthError?: { statusCode: number; headers: Record<string, string>; body: object } }).silkweaveAuthError
|
|
85
|
+
if (authError) {
|
|
86
|
+
return new Response(JSON.stringify(authError.body), {
|
|
87
|
+
status: authError.statusCode,
|
|
88
|
+
headers: { 'Content-Type': 'application/json', ...authError.headers }
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolveReady()
|
|
96
|
+
},
|
|
97
|
+
stop: async () => { /* no-op for fetch adapter */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dispatch: FetchHandler = async (request) => {
|
|
102
|
+
await ready
|
|
103
|
+
return handler(request)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
adapter,
|
|
108
|
+
handler: dispatch,
|
|
109
|
+
GET: dispatch,
|
|
110
|
+
POST: dispatch
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AuthConfig } from '@silkweave/auth'
|
|
2
|
+
import { AdapterFactory } from '@silkweave/core'
|
|
3
|
+
import { createHTTPHandler } from '@trpc/server/adapters/standalone'
|
|
4
|
+
import cors, { CorsOptions } from 'cors'
|
|
5
|
+
import http, { IncomingMessage, ServerResponse } from 'http'
|
|
6
|
+
import { buildRouter, TrpcHandlerContext } from '../lib/buildRouter.js'
|
|
7
|
+
import { createActionLogger, resolveAuth } from '../lib/createContext.js'
|
|
8
|
+
|
|
9
|
+
export interface TrpcAdapterOptions {
|
|
10
|
+
host?: string
|
|
11
|
+
port?: number
|
|
12
|
+
/** URL prefix stripped from incoming requests before tRPC routing. Default `/trpc/`. */
|
|
13
|
+
endpoint?: string
|
|
14
|
+
/** CORS configuration. `false` to disable, `true`/`undefined` for permissive defaults, or a CorsOptions object. */
|
|
15
|
+
cors?: CorsOptions | boolean
|
|
16
|
+
auth?: AuthConfig
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CorsMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void
|
|
20
|
+
|
|
21
|
+
function resolveCors(corsConfig: CorsOptions | boolean | undefined): CorsMiddleware | null {
|
|
22
|
+
if (corsConfig === false) { return null }
|
|
23
|
+
const userConfig = corsConfig === true || corsConfig === undefined ? {} : corsConfig
|
|
24
|
+
return cors({ origin: '*', ...userConfig }) as CorsMiddleware
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const trpc: AdapterFactory<TrpcAdapterOptions> = (options) => {
|
|
28
|
+
return (_silkweaveOptions, baseContext) => {
|
|
29
|
+
const context = baseContext.fork({ adapter: 'trpc' })
|
|
30
|
+
const host = options.host ?? '0.0.0.0'
|
|
31
|
+
const port = options.port ?? 8080
|
|
32
|
+
const endpoint = (options.endpoint ?? '/trpc/').replace(/\/?$/, '/')
|
|
33
|
+
let server: http.Server | undefined
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
context,
|
|
37
|
+
start: async (actions) => {
|
|
38
|
+
const router = buildRouter(actions)
|
|
39
|
+
const corsMiddleware = resolveCors(options.cors)
|
|
40
|
+
const logger = createActionLogger()
|
|
41
|
+
|
|
42
|
+
const createContext = async (
|
|
43
|
+
opts: { req: IncomingMessage; res: ServerResponse }
|
|
44
|
+
): Promise<TrpcHandlerContext> => {
|
|
45
|
+
const resolved = await resolveAuth(
|
|
46
|
+
options.auth,
|
|
47
|
+
opts.req.headers.authorization,
|
|
48
|
+
context.fork({ request: opts.req })
|
|
49
|
+
)
|
|
50
|
+
if (resolved.kind === 'error') {
|
|
51
|
+
for (const [key, value] of Object.entries(resolved.error.headers)) {
|
|
52
|
+
opts.res.setHeader(key, value)
|
|
53
|
+
}
|
|
54
|
+
opts.res.statusCode = resolved.error.statusCode
|
|
55
|
+
opts.res.setHeader('Content-Type', 'application/json')
|
|
56
|
+
opts.res.end(JSON.stringify(resolved.error.body))
|
|
57
|
+
throw new Error('Unauthorized')
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
silkweaveContext: context.fork({
|
|
61
|
+
logger,
|
|
62
|
+
request: opts.req,
|
|
63
|
+
...(resolved.authInfo ? { auth: resolved.authInfo } : {})
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const trpcHandler = createHTTPHandler({ router, basePath: endpoint, createContext })
|
|
69
|
+
|
|
70
|
+
const handler = (req: IncomingMessage, res: ServerResponse) => {
|
|
71
|
+
if (!req.url?.startsWith(endpoint)) {
|
|
72
|
+
res.statusCode = 404
|
|
73
|
+
res.setHeader('Content-Type', 'application/json')
|
|
74
|
+
res.end(JSON.stringify({ error: 'not_found', message: `No route for ${req.url}` }))
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
trpcHandler(req, res)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
server = http.createServer((req, res) => {
|
|
81
|
+
if (corsMiddleware) {
|
|
82
|
+
corsMiddleware(req, res, () => {
|
|
83
|
+
if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return }
|
|
84
|
+
handler(req, res)
|
|
85
|
+
})
|
|
86
|
+
} else {
|
|
87
|
+
handler(req, res)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await new Promise<void>((resolve) => {
|
|
92
|
+
server!.listen(port, host, () => { resolve() })
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
stop: async () => {
|
|
96
|
+
if (!server) { return }
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
server!.close((err) => {
|
|
99
|
+
if (err) { reject(err) } else { resolve() }
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './adapter/fetch.js'
|
|
2
|
+
export * from './adapter/trpc.js'
|
|
3
|
+
export { buildRouter } from './lib/buildRouter.js'
|
|
4
|
+
export type { TrpcHandlerContext } from './lib/buildRouter.js'
|
|
5
|
+
export { mapError } from './lib/errors.js'
|
|
6
|
+
export type { InferTrpcRouter } from './lib/inferRouter.js'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Action, SilkweaveContext } from '@silkweave/core'
|
|
3
|
+
import { initTRPC } from '@trpc/server'
|
|
4
|
+
import { camelCase } from 'change-case'
|
|
5
|
+
import { mapError } from './errors.js'
|
|
6
|
+
|
|
7
|
+
export interface TrpcHandlerContext {
|
|
8
|
+
silkweaveContext: SilkweaveContext
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildRouter(actions: Action[]) {
|
|
12
|
+
const t = initTRPC.context<TrpcHandlerContext>().create()
|
|
13
|
+
const record: Record<string, any> = {}
|
|
14
|
+
for (const action of actions) {
|
|
15
|
+
const key = camelCase(action.name)
|
|
16
|
+
const base = t.procedure.input(action.input)
|
|
17
|
+
const handler = async ({ input, ctx }: { input: object; ctx: TrpcHandlerContext }) => {
|
|
18
|
+
try {
|
|
19
|
+
return await action.run(input, ctx.silkweaveContext)
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw mapError(error)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
record[key] = action.kind === 'query' ? base.query(handler) : base.mutation(handler)
|
|
25
|
+
}
|
|
26
|
+
return t.router(record)
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AuthConfig, AuthInfo, validateToken } from '@silkweave/auth'
|
|
2
|
+
import { SilkweaveContext } from '@silkweave/core'
|
|
3
|
+
import { buildLogLevels, Logger, LogLevel } from '@silkweave/logger'
|
|
4
|
+
|
|
5
|
+
const CONSOLE_LEVEL_MAP: Record<LogLevel, 'log' | 'info' | 'warn' | 'error'> = {
|
|
6
|
+
emergency: 'error',
|
|
7
|
+
alert: 'error',
|
|
8
|
+
critical: 'error',
|
|
9
|
+
error: 'error',
|
|
10
|
+
warning: 'warn',
|
|
11
|
+
notice: 'info',
|
|
12
|
+
info: 'info',
|
|
13
|
+
debug: 'log'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createActionLogger(): Logger {
|
|
17
|
+
return {
|
|
18
|
+
...buildLogLevels((level, data) => {
|
|
19
|
+
console[CONSOLE_LEVEL_MAP[level]](data)
|
|
20
|
+
}),
|
|
21
|
+
progress: () => { /* progress notifications not supported on tRPC HTTP */ }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthErrorPayload {
|
|
26
|
+
statusCode: number
|
|
27
|
+
headers: Record<string, string>
|
|
28
|
+
body: { error: string; error_description: string }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ResolvedAuth =
|
|
32
|
+
| { kind: 'ok'; authInfo?: AuthInfo }
|
|
33
|
+
| { kind: 'error'; error: AuthErrorPayload }
|
|
34
|
+
|
|
35
|
+
export async function resolveAuth(
|
|
36
|
+
auth: AuthConfig | undefined,
|
|
37
|
+
authHeader: string | null | undefined,
|
|
38
|
+
context: SilkweaveContext
|
|
39
|
+
): Promise<ResolvedAuth> {
|
|
40
|
+
if (!auth) { return { kind: 'ok' } }
|
|
41
|
+
const result = await validateToken(authHeader, auth, context)
|
|
42
|
+
if (result.error) { return { kind: 'error', error: result.error } }
|
|
43
|
+
return { kind: 'ok', authInfo: result.auth }
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SilkweaveError } from '@silkweave/core'
|
|
2
|
+
import { TRPCError } from '@trpc/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
type TRPCErrorCode = TRPCError['code']
|
|
6
|
+
|
|
7
|
+
const CODE_MAP: Record<number, TRPCErrorCode> = {
|
|
8
|
+
400: 'BAD_REQUEST',
|
|
9
|
+
401: 'UNAUTHORIZED',
|
|
10
|
+
403: 'FORBIDDEN',
|
|
11
|
+
404: 'NOT_FOUND',
|
|
12
|
+
408: 'TIMEOUT',
|
|
13
|
+
409: 'CONFLICT',
|
|
14
|
+
412: 'PRECONDITION_FAILED',
|
|
15
|
+
413: 'PAYLOAD_TOO_LARGE',
|
|
16
|
+
429: 'TOO_MANY_REQUESTS',
|
|
17
|
+
499: 'CLIENT_CLOSED_REQUEST',
|
|
18
|
+
500: 'INTERNAL_SERVER_ERROR',
|
|
19
|
+
501: 'NOT_IMPLEMENTED',
|
|
20
|
+
502: 'BAD_GATEWAY',
|
|
21
|
+
503: 'SERVICE_UNAVAILABLE',
|
|
22
|
+
504: 'GATEWAY_TIMEOUT'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mapError(error: unknown): TRPCError {
|
|
26
|
+
if (error instanceof TRPCError) { return error }
|
|
27
|
+
if (error instanceof z.ZodError) {
|
|
28
|
+
return new TRPCError({ code: 'BAD_REQUEST', message: error.message, cause: error })
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof SilkweaveError) {
|
|
31
|
+
return new TRPCError({
|
|
32
|
+
code: CODE_MAP[error.statusCode] ?? 'INTERNAL_SERVER_ERROR',
|
|
33
|
+
message: error.message,
|
|
34
|
+
cause: error
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return new TRPCError({
|
|
38
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
39
|
+
message: error instanceof Error ? error.message : 'Internal error',
|
|
40
|
+
cause: error instanceof Error ? error : undefined
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Action, Silkweave } from '@silkweave/core'
|
|
2
|
+
import type {
|
|
3
|
+
AnyTRPCRootTypes,
|
|
4
|
+
TRPCBuiltRouter,
|
|
5
|
+
TRPCMutationProcedure,
|
|
6
|
+
TRPCQueryProcedure
|
|
7
|
+
} from '@trpc/server'
|
|
8
|
+
import type { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
type CamelCase<S extends string> = S extends `${infer A}-${infer B}`
|
|
11
|
+
? `${A}${Capitalize<CamelCase<B>>}`
|
|
12
|
+
: S extends `${infer A}_${infer B}`
|
|
13
|
+
? `${A}${Capitalize<CamelCase<B>>}`
|
|
14
|
+
: S
|
|
15
|
+
|
|
16
|
+
type ActionToProcedure<A extends Action> = 'query' extends NonNullable<A['kind']>
|
|
17
|
+
? TRPCQueryProcedure<{
|
|
18
|
+
meta: object
|
|
19
|
+
input: z.infer<A['input']>
|
|
20
|
+
output: Awaited<ReturnType<A['run']>>
|
|
21
|
+
}>
|
|
22
|
+
: TRPCMutationProcedure<{
|
|
23
|
+
meta: object
|
|
24
|
+
input: z.infer<A['input']>
|
|
25
|
+
output: Awaited<ReturnType<A['run']>>
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
type ActionsToRouterRecord<Actions extends Record<string, Action>> = {
|
|
29
|
+
[K in keyof Actions & string as CamelCase<Actions[K]['name']>]: ActionToProcedure<Actions[K]>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TrpcRootTypes = {
|
|
33
|
+
ctx: object
|
|
34
|
+
meta: object
|
|
35
|
+
errorShape: unknown
|
|
36
|
+
transformer: false
|
|
37
|
+
} & AnyTRPCRootTypes
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extracts a fully-typed tRPC router from a Silkweave builder instance.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* ```ts
|
|
44
|
+
* const server = silkweave(opts).adapter(trpc({ port: 8080 })).action(HelloAction)
|
|
45
|
+
* export type AppRouter = InferTrpcRouter<typeof server>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export type InferTrpcRouter<S> = S extends Silkweave<infer Actions>
|
|
49
|
+
? TRPCBuiltRouter<TrpcRootTypes, ActionsToRouterRecord<Actions>>
|
|
50
|
+
: never
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "build",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"composite": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
18
|
+
"sourceMap": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"],
|
|
21
|
+
"exclude": ["node_modules", "build"]
|
|
22
|
+
}
|