weifuwu 0.25.2 → 0.27.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 +291 -2489
- package/ai/provider.ts +129 -0
- package/ai/stream.ts +63 -0
- package/cli.ts +55 -257
- package/core/cookie.ts +114 -0
- package/core/env.ts +142 -0
- package/core/logger.ts +72 -0
- package/core/router.ts +795 -0
- package/core/serve.ts +294 -0
- package/core/sse.ts +85 -0
- package/core/trace.ts +146 -0
- package/graphql.ts +267 -0
- package/hub.ts +133 -0
- package/index.ts +71 -0
- package/mailer.ts +81 -0
- package/middleware/compress.ts +103 -0
- package/middleware/cors.ts +81 -0
- package/middleware/csrf.ts +112 -0
- package/middleware/flash.ts +144 -0
- package/middleware/health.ts +44 -0
- package/middleware/helmet.ts +98 -0
- package/middleware/i18n.ts +175 -0
- package/middleware/rate-limit.ts +167 -0
- package/middleware/request-id.ts +60 -0
- package/middleware/static.ts +149 -0
- package/middleware/theme.ts +84 -0
- package/middleware/upload.ts +168 -0
- package/middleware/validate.ts +186 -0
- package/package.json +14 -36
- package/postgres/client.ts +132 -0
- package/postgres/index.ts +4 -0
- package/postgres/module.ts +37 -0
- package/postgres/schema/columns.ts +186 -0
- package/postgres/schema/index.ts +36 -0
- package/postgres/schema/sql.ts +39 -0
- package/postgres/schema/table.ts +548 -0
- package/postgres/schema/where.ts +99 -0
- package/postgres/types.ts +48 -0
- package/queue/cron.ts +90 -0
- package/queue/index.ts +654 -0
- package/queue/types.ts +60 -0
- package/redis/client.ts +24 -0
- package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
- package/redis/types.ts +28 -0
- package/types.ts +78 -0
- package/cli/template/app.ts +0 -22
- package/cli/template/index.ts +0 -10
- package/cli/template/locales/en.json +0 -13
- package/cli/template/locales/zh-CN.json +0 -13
- package/cli/template/locales/zh-TW.json +0 -13
- package/cli/template/locales/zh.json +0 -13
- package/cli/template/ui/app/globals.css +0 -2
- package/cli/template/ui/app/layout.tsx +0 -15
- package/cli/template/ui/app/page.tsx +0 -124
- package/cli/template/ui/components/Greeting.tsx +0 -3
- package/dist/agent/client.d.ts +0 -2
- package/dist/agent/index.d.ts +0 -2
- package/dist/agent/rest.d.ts +0 -14
- package/dist/agent/run.d.ts +0 -19
- package/dist/agent/types.d.ts +0 -55
- package/dist/ai/provider.d.ts +0 -45
- package/dist/ai/utils.d.ts +0 -5
- package/dist/ai/workflow.d.ts +0 -17
- package/dist/ai-sdk.d.ts +0 -2
- package/dist/ai.d.ts +0 -13
- package/dist/analytics.d.ts +0 -45
- package/dist/auth.d.ts +0 -22
- package/dist/cache.d.ts +0 -74
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -302
- package/dist/client-locale.d.ts +0 -25
- package/dist/client-pref.d.ts +0 -3
- package/dist/client-router.d.ts +0 -300
- package/dist/client-state.d.ts +0 -22
- package/dist/client-theme.d.ts +0 -36
- package/dist/compile.d.ts +0 -15
- package/dist/compress.d.ts +0 -20
- package/dist/cookie.d.ts +0 -36
- package/dist/cors.d.ts +0 -25
- package/dist/cron-utils.d.ts +0 -73
- package/dist/csrf.d.ts +0 -47
- package/dist/deploy/config.d.ts +0 -2
- package/dist/deploy/gateway.d.ts +0 -2
- package/dist/deploy/index.d.ts +0 -4
- package/dist/deploy/manager.d.ts +0 -16
- package/dist/deploy/process.d.ts +0 -14
- package/dist/deploy/types.d.ts +0 -53
- package/dist/env.d.ts +0 -69
- package/dist/error-boundary.d.ts +0 -2
- package/dist/flash.d.ts +0 -90
- package/dist/fts.d.ts +0 -36
- package/dist/graphql.d.ts +0 -16
- package/dist/head.d.ts +0 -6
- package/dist/health.d.ts +0 -24
- package/dist/helmet.d.ts +0 -33
- package/dist/html-shell.d.ts +0 -1
- package/dist/hub.d.ts +0 -37
- package/dist/i18n.d.ts +0 -39
- package/dist/iii/client.d.ts +0 -2
- package/dist/iii/index.d.ts +0 -4
- package/dist/iii/register-worker.d.ts +0 -9
- package/dist/iii/rest.d.ts +0 -3
- package/dist/iii/stream.d.ts +0 -82
- package/dist/iii/types.d.ts +0 -121
- package/dist/iii/worker.d.ts +0 -2
- package/dist/iii/ws.d.ts +0 -22
- package/dist/index.d.ts +0 -101
- package/dist/index.js +0 -12752
- package/dist/kb/index.d.ts +0 -3
- package/dist/kb/types.d.ts +0 -72
- package/dist/layout.d.ts +0 -2
- package/dist/live.d.ts +0 -7
- package/dist/logdb/client.d.ts +0 -2
- package/dist/logdb/index.d.ts +0 -2
- package/dist/logdb/rest.d.ts +0 -5
- package/dist/logdb/types.d.ts +0 -27
- package/dist/logger.d.ts +0 -16
- package/dist/mailer.d.ts +0 -51
- package/dist/mcp.d.ts +0 -34
- package/dist/messager/agent.d.ts +0 -11
- package/dist/messager/client.d.ts +0 -2
- package/dist/messager/index.d.ts +0 -2
- package/dist/messager/rest.d.ts +0 -15
- package/dist/messager/types.d.ts +0 -57
- package/dist/messager/ws.d.ts +0 -14
- package/dist/module-server.d.ts +0 -9
- package/dist/not-found.d.ts +0 -2
- package/dist/notifier/client.d.ts +0 -2
- package/dist/notifier/index.d.ts +0 -2
- package/dist/notifier/types.d.ts +0 -105
- package/dist/opencode/client.d.ts +0 -2
- package/dist/opencode/index.d.ts +0 -2
- package/dist/opencode/permissions.d.ts +0 -5
- package/dist/opencode/prompt.d.ts +0 -8
- package/dist/opencode/rest.d.ts +0 -16
- package/dist/opencode/run.d.ts +0 -13
- package/dist/opencode/session.d.ts +0 -26
- package/dist/opencode/skills.d.ts +0 -4
- package/dist/opencode/tools/bash.d.ts +0 -6
- package/dist/opencode/tools/edit.d.ts +0 -19
- package/dist/opencode/tools/glob.d.ts +0 -9
- package/dist/opencode/tools/grep.d.ts +0 -17
- package/dist/opencode/tools/index.d.ts +0 -12
- package/dist/opencode/tools/question.d.ts +0 -5
- package/dist/opencode/tools/read.d.ts +0 -16
- package/dist/opencode/tools/skill.d.ts +0 -18
- package/dist/opencode/tools/web.d.ts +0 -18
- package/dist/opencode/tools/write.d.ts +0 -13
- package/dist/opencode/types.d.ts +0 -90
- package/dist/opencode/ws.d.ts +0 -21
- package/dist/permissions.d.ts +0 -51
- package/dist/postgres/client.d.ts +0 -4
- package/dist/postgres/index.d.ts +0 -4
- package/dist/postgres/module.d.ts +0 -17
- package/dist/postgres/schema/columns.d.ts +0 -99
- package/dist/postgres/schema/index.d.ts +0 -6
- package/dist/postgres/schema/sql.d.ts +0 -22
- package/dist/postgres/schema/table.d.ts +0 -141
- package/dist/postgres/schema/where.d.ts +0 -29
- package/dist/postgres/types.d.ts +0 -50
- package/dist/queue/index.d.ts +0 -2
- package/dist/queue/types.d.ts +0 -62
- package/dist/rate-limit.d.ts +0 -45
- package/dist/react.d.ts +0 -14
- package/dist/react.js +0 -751
- package/dist/redis/client.d.ts +0 -2
- package/dist/redis/types.d.ts +0 -18
- package/dist/request-id.d.ts +0 -40
- package/dist/router.d.ts +0 -73
- package/dist/s3.d.ts +0 -68
- package/dist/seo.d.ts +0 -104
- package/dist/serve.d.ts +0 -38
- package/dist/server-registry.d.ts +0 -10
- package/dist/session.d.ts +0 -117
- package/dist/sse.d.ts +0 -47
- package/dist/ssr-entries.d.ts +0 -4
- package/dist/ssr.d.ts +0 -11
- package/dist/static.d.ts +0 -23
- package/dist/stream.d.ts +0 -24
- package/dist/tailwind.d.ts +0 -15
- package/dist/tenant/client.d.ts +0 -2
- package/dist/tenant/graphql.d.ts +0 -3
- package/dist/tenant/index.d.ts +0 -2
- package/dist/tenant/rest.d.ts +0 -3
- package/dist/tenant/schema.d.ts +0 -5
- package/dist/tenant/types.d.ts +0 -48
- package/dist/tenant/utils.d.ts +0 -9
- package/dist/test-utils.d.ts +0 -194
- package/dist/theme.d.ts +0 -31
- package/dist/trace.d.ts +0 -95
- package/dist/tsx-context.d.ts +0 -32
- package/dist/types.d.ts +0 -47
- package/dist/upload.d.ts +0 -55
- package/dist/use-action.d.ts +0 -42
- package/dist/use-agent-stream.d.ts +0 -49
- package/dist/use-flash-message.d.ts +0 -17
- package/dist/use-websocket.d.ts +0 -42
- package/dist/user/client.d.ts +0 -30
- package/dist/user/index.d.ts +0 -2
- package/dist/user/oauth-login.d.ts +0 -21
- package/dist/user/oauth2.d.ts +0 -31
- package/dist/user/types.d.ts +0 -178
- package/dist/validate.d.ts +0 -32
- package/dist/vendor.d.ts +0 -7
- package/dist/webhook.d.ts +0 -79
- package/opencode/ui/app/globals.css +0 -1
- package/opencode/ui/app/layout.tsx +0 -13
- package/opencode/ui/app/page.tsx +0 -523
package/graphql.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
|
2
|
+
import {
|
|
3
|
+
buildSchema,
|
|
4
|
+
graphql as executeGraphQL,
|
|
5
|
+
type GraphQLSchema,
|
|
6
|
+
validate as validateQuery,
|
|
7
|
+
parse,
|
|
8
|
+
type DocumentNode,
|
|
9
|
+
} from 'graphql'
|
|
10
|
+
import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
11
|
+
import type { Context } from './types.ts'
|
|
12
|
+
import { Router } from './core/router.ts'
|
|
13
|
+
import { currentTraceId } from './core/trace.ts'
|
|
14
|
+
|
|
15
|
+
export interface GraphQLOptions {
|
|
16
|
+
schema: string | GraphQLSchema
|
|
17
|
+
rootValue?: any
|
|
18
|
+
resolvers?: any
|
|
19
|
+
context?: (req: Request, ctx: Context) => Record<string, any> | Promise<Record<string, any>>
|
|
20
|
+
graphiql?: boolean
|
|
21
|
+
/** Max query depth (nesting). Default: 10. Set 0 to disable. */
|
|
22
|
+
maxDepth?: number
|
|
23
|
+
/** Execution timeout in ms. Default: 30_000. */
|
|
24
|
+
timeout?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type GraphQLHandler = (
|
|
28
|
+
req: Request,
|
|
29
|
+
ctx: Context,
|
|
30
|
+
) => GraphQLOptions | Promise<GraphQLOptions>
|
|
31
|
+
|
|
32
|
+
type GraphQLParams = {
|
|
33
|
+
query: string
|
|
34
|
+
variables: Record<string, any>
|
|
35
|
+
operationName?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseParamsFromGet(url: URL): GraphQLParams | null {
|
|
39
|
+
const query = url.searchParams.get('query')
|
|
40
|
+
if (!query) return null
|
|
41
|
+
let variables = {}
|
|
42
|
+
const variablesStr = url.searchParams.get('variables')
|
|
43
|
+
if (variablesStr) {
|
|
44
|
+
try {
|
|
45
|
+
variables = JSON.parse(variablesStr)
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { query, variables, operationName: url.searchParams.get('operationName') || undefined }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function parseParamsFromPost(req: Request): Promise<GraphQLParams | null> {
|
|
54
|
+
try {
|
|
55
|
+
const body = (await req.json()) as {
|
|
56
|
+
query?: string
|
|
57
|
+
variables?: Record<string, any>
|
|
58
|
+
operationName?: string
|
|
59
|
+
}
|
|
60
|
+
if (!body.query) return null
|
|
61
|
+
return { query: body.query, variables: body.variables || {}, operationName: body.operationName }
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildSchemaFromOptions(options: GraphQLOptions): GraphQLSchema {
|
|
68
|
+
try {
|
|
69
|
+
if (typeof options.schema === 'string') {
|
|
70
|
+
return options.resolvers
|
|
71
|
+
? makeExecutableSchema({ typeDefs: options.schema, resolvers: options.resolvers })
|
|
72
|
+
: buildSchema(options.schema)
|
|
73
|
+
}
|
|
74
|
+
return options.schema
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
77
|
+
console.error(`[graphql] schema build failed: ${msg}`)
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Count max nesting depth of a GraphQL query. */
|
|
83
|
+
function queryDepth(doc: DocumentNode): number {
|
|
84
|
+
let max = 0
|
|
85
|
+
function walk(node: any, depth: number) {
|
|
86
|
+
if (depth > max) max = depth
|
|
87
|
+
if (node.selectionSet) {
|
|
88
|
+
for (const sel of node.selectionSet.selections) {
|
|
89
|
+
walk(sel, depth + 1)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const def of doc.definitions) {
|
|
94
|
+
if (def.kind === 'OperationDefinition') {
|
|
95
|
+
walk(def, 0)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return max
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function executeQuery(
|
|
102
|
+
schema: GraphQLSchema,
|
|
103
|
+
params: GraphQLParams,
|
|
104
|
+
options: GraphQLOptions,
|
|
105
|
+
req: Request,
|
|
106
|
+
ctx: Context,
|
|
107
|
+
): Promise<Response> {
|
|
108
|
+
// Depth limit
|
|
109
|
+
const maxDepth = options.maxDepth ?? 10
|
|
110
|
+
if (maxDepth > 0) {
|
|
111
|
+
try {
|
|
112
|
+
const doc = parse(params.query)
|
|
113
|
+
const depth = queryDepth(doc)
|
|
114
|
+
if (depth > maxDepth) {
|
|
115
|
+
return Response.json(
|
|
116
|
+
{ errors: [{ message: `Query depth ${depth} exceeds limit ${maxDepth}` }] },
|
|
117
|
+
{ status: 400 },
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
const validationErrors = validateQuery(schema, doc)
|
|
121
|
+
if (validationErrors.length > 0) {
|
|
122
|
+
return Response.json(
|
|
123
|
+
{ errors: validationErrors.map((e) => ({ message: e.message })) },
|
|
124
|
+
{ status: 400 },
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
129
|
+
return Response.json({ errors: [{ message: `Parse error: ${msg}` }] }, { status: 400 })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Timeout
|
|
134
|
+
const timeout = options.timeout ?? 30_000
|
|
135
|
+
const contextValue = options.context ? await options.context(req, ctx) : ctx
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const resultPromise = executeGraphQL({
|
|
139
|
+
schema,
|
|
140
|
+
source: params.query,
|
|
141
|
+
rootValue: options.rootValue,
|
|
142
|
+
contextValue,
|
|
143
|
+
variableValues: params.variables,
|
|
144
|
+
operationName: params.operationName,
|
|
145
|
+
}) as any
|
|
146
|
+
|
|
147
|
+
let result
|
|
148
|
+
if (timeout > 0) {
|
|
149
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
150
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
151
|
+
timer = setTimeout(() => reject(new Error('Query timeout')), timeout)
|
|
152
|
+
})
|
|
153
|
+
result = await Promise.race([resultPromise, timeoutPromise])
|
|
154
|
+
if (timer) clearTimeout(timer)
|
|
155
|
+
} else {
|
|
156
|
+
result = await resultPromise
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Response.json(result, { status: result.errors ? 400 : 200 })
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
162
|
+
console.error(`[${currentTraceId()}] graphql execution failed: ${msg}`)
|
|
163
|
+
return Response.json({ errors: [{ message: msg }] }, { status: 500 })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function graphiqlHTML(endpoint: string): string {
|
|
168
|
+
const safeEndpoint = endpoint.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\x3C')
|
|
169
|
+
return `<!doctype html>
|
|
170
|
+
<html lang="en">
|
|
171
|
+
<head>
|
|
172
|
+
<meta charset="UTF-8" />
|
|
173
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
174
|
+
<title>GraphiQL</title>
|
|
175
|
+
<style>body { margin: 0; } #graphiql { height: 100dvh; }</style>
|
|
176
|
+
<link rel="stylesheet" href="https://esm.sh/graphiql@5.2.2/dist/style.css" />
|
|
177
|
+
<script type="importmap">
|
|
178
|
+
{
|
|
179
|
+
"imports": {
|
|
180
|
+
"react": "https://esm.sh/react@19.2.5",
|
|
181
|
+
"react/": "https://esm.sh/react@19.2.5/",
|
|
182
|
+
"react-dom": "https://esm.sh/react-dom@19.2.5",
|
|
183
|
+
"react-dom/": "https://esm.sh/react-dom@19.2.5/",
|
|
184
|
+
"graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
|
|
185
|
+
"graphiql/": "https://esm.sh/graphiql@5.2.2/",
|
|
186
|
+
"@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
|
|
187
|
+
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
|
|
188
|
+
"graphql": "https://esm.sh/graphql@16.13.2",
|
|
189
|
+
"@emotion/is-prop-valid": "data:text/javascript,"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
193
|
+
<script type="module">
|
|
194
|
+
import React from 'react';
|
|
195
|
+
import ReactDOM from 'react-dom/client';
|
|
196
|
+
import { GraphiQL } from 'graphiql';
|
|
197
|
+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|
198
|
+
import 'graphiql/setup-workers/esm.sh';
|
|
199
|
+
|
|
200
|
+
const fetcher = createGraphiQLFetcher({ url: "${safeEndpoint}" });
|
|
201
|
+
|
|
202
|
+
function App() {
|
|
203
|
+
return React.createElement(GraphiQL, { fetcher });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const container = document.getElementById('graphiql');
|
|
207
|
+
const root = ReactDOM.createRoot(container);
|
|
208
|
+
root.render(React.createElement(App));
|
|
209
|
+
</script>
|
|
210
|
+
</head>
|
|
211
|
+
<body>
|
|
212
|
+
<div id="graphiql">Loading\u2026</div>
|
|
213
|
+
</body>
|
|
214
|
+
</html>`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function graphql(handler: GraphQLHandler): Router {
|
|
218
|
+
const r = new Router()
|
|
219
|
+
let cachedOptions: GraphQLOptions | null = null
|
|
220
|
+
let cachedSchema: GraphQLSchema | null = null
|
|
221
|
+
|
|
222
|
+
async function getSchema(
|
|
223
|
+
req: Request,
|
|
224
|
+
ctx: Context,
|
|
225
|
+
): Promise<{ options: GraphQLOptions; schema: GraphQLSchema }> {
|
|
226
|
+
const options = await handler(req, ctx)
|
|
227
|
+
// Cache schema — handler must return the same schema reference for cache to work.
|
|
228
|
+
// If schema changes (e.g. hot-reload), return a different object reference.
|
|
229
|
+
if (cachedSchema && cachedOptions === options) {
|
|
230
|
+
return { options, schema: cachedSchema }
|
|
231
|
+
}
|
|
232
|
+
const schema = buildSchemaFromOptions(options)
|
|
233
|
+
cachedOptions = options
|
|
234
|
+
cachedSchema = schema
|
|
235
|
+
return { options, schema }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
r.get('/', async (req, ctx) => {
|
|
239
|
+
const { options, schema } = await getSchema(req, ctx)
|
|
240
|
+
const url = new URL(req.url)
|
|
241
|
+
|
|
242
|
+
if (options.graphiql && !url.searchParams.has('query')) {
|
|
243
|
+
return new Response(graphiqlHTML(url.pathname), {
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: { 'Content-Type': 'text/html' },
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const params = parseParamsFromGet(url)
|
|
250
|
+
if (!params) {
|
|
251
|
+
return Response.json({ errors: [{ message: 'Missing query' }] }, { status: 400 })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return executeQuery(schema, params, options, req, ctx)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
r.post('/', async (req, ctx) => {
|
|
258
|
+
const { options, schema } = await getSchema(req, ctx)
|
|
259
|
+
const params = await parseParamsFromPost(req)
|
|
260
|
+
if (!params) {
|
|
261
|
+
return Response.json({ errors: [{ message: 'Missing query' }] }, { status: 400 })
|
|
262
|
+
}
|
|
263
|
+
return executeQuery(schema, params, options, req, ctx)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return r
|
|
267
|
+
}
|
package/hub.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Redis, WebSocket, Closeable } from './types.ts'
|
|
2
|
+
|
|
3
|
+
/** Options for {@link createHub}. */
|
|
4
|
+
export interface HubOptions {
|
|
5
|
+
/** Optional Redis client for cross-process pub/sub broadcast. */
|
|
6
|
+
redis?: Redis
|
|
7
|
+
/** Key prefix for Redis channels (default: `'hub:'`). */
|
|
8
|
+
prefix?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* In-memory (and optionally Redis-backed) pub/sub hub for WebSocket rooms.
|
|
13
|
+
*
|
|
14
|
+
* Used internally by the WebSocket handler to implement `ctx.ws.join()` / `ctx.ws.sendRoom()`. */
|
|
15
|
+
export interface Hub extends Closeable {
|
|
16
|
+
/** Subscribe a WebSocket to a room/group. */
|
|
17
|
+
join(key: string, ws: WebSocket): void
|
|
18
|
+
/** Unsubscribe a WebSocket from all rooms. */
|
|
19
|
+
leave(ws: WebSocket): void
|
|
20
|
+
/** Send a JSON message to all members of a room. */
|
|
21
|
+
broadcast(key: string, data: unknown): void
|
|
22
|
+
/** Close the hub, disconnect Redis subscribers, clear all rooms. */
|
|
23
|
+
close(): Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a pub/sub hub for WebSocket room management.
|
|
28
|
+
*
|
|
29
|
+
* In-memory by default. Pass `redis` to enable cross-process broadcasting.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createHub } from 'weifuwu'
|
|
33
|
+
*
|
|
34
|
+
* const hub = createHub()
|
|
35
|
+
* hub.join('room:general', ws)
|
|
36
|
+
* hub.broadcast('room:general', { type: 'chat', text: 'Hello' })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createHub(opts?: HubOptions): Hub {
|
|
40
|
+
const prefix = opts?.prefix ?? 'hub:'
|
|
41
|
+
const channels = new Map<string, Set<WebSocket>>()
|
|
42
|
+
const wsKeys = new Map<WebSocket, Set<string>>()
|
|
43
|
+
|
|
44
|
+
let redisPub: Redis | undefined
|
|
45
|
+
let redisSub: Redis | null = null
|
|
46
|
+
|
|
47
|
+
if (opts?.redis) {
|
|
48
|
+
redisPub = opts.redis
|
|
49
|
+
redisSub = opts.redis.duplicate()
|
|
50
|
+
redisSub.on('message', (rawChannel: string, rawData: string) => {
|
|
51
|
+
if (!rawChannel.startsWith(prefix)) return
|
|
52
|
+
const key = rawChannel.slice(prefix.length)
|
|
53
|
+
const members = channels.get(key)
|
|
54
|
+
if (!members) return
|
|
55
|
+
for (const ws of members) {
|
|
56
|
+
try {
|
|
57
|
+
ws.send(rawData)
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function join(key: string, ws: WebSocket): void {
|
|
64
|
+
if (!channels.has(key)) {
|
|
65
|
+
channels.set(key, new Set())
|
|
66
|
+
redisSub?.subscribe(`${prefix}${key}`)
|
|
67
|
+
}
|
|
68
|
+
channels.get(key)!.add(ws)
|
|
69
|
+
let keys = wsKeys.get(ws)
|
|
70
|
+
if (!keys) {
|
|
71
|
+
keys = new Set()
|
|
72
|
+
wsKeys.set(ws, keys)
|
|
73
|
+
}
|
|
74
|
+
keys.add(key)
|
|
75
|
+
// Auto-cleanup on close (if WebSocket supports addEventListener)
|
|
76
|
+
if (typeof ws.addEventListener === 'function') {
|
|
77
|
+
ws.addEventListener('close', () => removeFromChannels(ws))
|
|
78
|
+
ws.addEventListener('error', () => removeFromChannels(ws))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeFromChannels(ws: WebSocket): void {
|
|
83
|
+
const keys = wsKeys.get(ws)
|
|
84
|
+
if (keys) {
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const members = channels.get(key)
|
|
87
|
+
if (members) {
|
|
88
|
+
members.delete(ws)
|
|
89
|
+
if (members.size === 0) channels.delete(key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
wsKeys.delete(ws)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function leave(ws: WebSocket): void {
|
|
97
|
+
removeFromChannels(ws)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function broadcast(key: string, data: unknown): void {
|
|
101
|
+
const msg = JSON.stringify(data)
|
|
102
|
+
const members = channels.get(key)
|
|
103
|
+
if (members) {
|
|
104
|
+
const dead: WebSocket[] = []
|
|
105
|
+
for (const ws of members) {
|
|
106
|
+
try {
|
|
107
|
+
ws.send(msg)
|
|
108
|
+
} catch {
|
|
109
|
+
dead.push(ws)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const ws of dead) removeFromChannels(ws)
|
|
113
|
+
}
|
|
114
|
+
redisPub?.publish(`${prefix}${key}`, msg)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function close(): Promise<void> {
|
|
118
|
+
// Disconnect stale WebSocket listeners
|
|
119
|
+
for (const ws of wsKeys.keys()) {
|
|
120
|
+
removeFromChannels(ws)
|
|
121
|
+
}
|
|
122
|
+
channels.clear()
|
|
123
|
+
wsKeys.clear()
|
|
124
|
+
if (redisSub) {
|
|
125
|
+
redisSub.removeAllListeners('message')
|
|
126
|
+
await redisSub.quit()
|
|
127
|
+
}
|
|
128
|
+
redisPub = undefined
|
|
129
|
+
redisSub = null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { join, leave, broadcast, close }
|
|
133
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type { Context, Handler, Middleware, ErrorHandler } from './types.ts'
|
|
2
|
+
export { HttpError } from './types.ts'
|
|
3
|
+
export { currentTraceId, currentTrace, runWithTrace, traceElapsed, trace } from './core/trace.ts'
|
|
4
|
+
export type { TraceContext, TraceInjected, TraceOptions } from './core/trace.ts'
|
|
5
|
+
export { loadEnv, isDev, isProd, isBundled, getPublicEnv, env } from './core/env.ts'
|
|
6
|
+
export { serve, createTestServer, DEFAULT_MAX_BODY } from './core/serve.ts'
|
|
7
|
+
export type { ServeOptions, Server } from './core/serve.ts'
|
|
8
|
+
export { Router } from './core/router.ts'
|
|
9
|
+
export type { WebSocketHandler } from './core/router.ts'
|
|
10
|
+
export { logger } from './core/logger.ts'
|
|
11
|
+
export type { LoggerOptions } from './core/logger.ts'
|
|
12
|
+
export { cors } from './middleware/cors.ts'
|
|
13
|
+
export type { CORSOptions } from './middleware/cors.ts'
|
|
14
|
+
|
|
15
|
+
export { serveStatic } from './middleware/static.ts'
|
|
16
|
+
export type { ServeStaticOptions } from './middleware/static.ts'
|
|
17
|
+
export { validate } from './middleware/validate.ts'
|
|
18
|
+
export type { ValidationSchemas, ValidateModule } from './middleware/validate.ts'
|
|
19
|
+
export { getCookies, setCookie, deleteCookie } from './core/cookie.ts'
|
|
20
|
+
export type { CookieOptions } from './core/cookie.ts'
|
|
21
|
+
export { upload } from './middleware/upload.ts'
|
|
22
|
+
export type { UploadOptions, UploadedFile, UploadModule } from './middleware/upload.ts'
|
|
23
|
+
export { rateLimit } from './middleware/rate-limit.ts'
|
|
24
|
+
export type { RateLimitOptions } from './middleware/rate-limit.ts'
|
|
25
|
+
export { compress } from './middleware/compress.ts'
|
|
26
|
+
export type { CompressOptions } from './middleware/compress.ts'
|
|
27
|
+
export { helmet } from './middleware/helmet.ts'
|
|
28
|
+
export type { HelmetOptions } from './middleware/helmet.ts'
|
|
29
|
+
export { requestId } from './middleware/request-id.ts'
|
|
30
|
+
export type { RequestIdOptions, RequestIdModule } from './middleware/request-id.ts'
|
|
31
|
+
export { createSSEStream, formatSSE, formatSSEData } from './core/sse.ts'
|
|
32
|
+
export type { SSEEvent } from './core/sse.ts'
|
|
33
|
+
export { testApp, TestApp, TestRequest, createTestDb, withTestDb } from './test/test-utils.ts'
|
|
34
|
+
export type { TestResponse, TestDb } from './test/test-utils.ts'
|
|
35
|
+
export { graphql } from './graphql.ts'
|
|
36
|
+
export type { GraphQLOptions, GraphQLHandler } from './graphql.ts'
|
|
37
|
+
export { aiStream } from './ai/stream.ts'
|
|
38
|
+
export type { AIHandler } from './ai/stream.ts'
|
|
39
|
+
export { aiProvider } from './ai/provider.ts'
|
|
40
|
+
export type { AIProvider, AIProviderOptions, AIProviderInjected } from './ai/provider.ts'
|
|
41
|
+
export {
|
|
42
|
+
streamText,
|
|
43
|
+
generateText,
|
|
44
|
+
generateObject,
|
|
45
|
+
streamObject,
|
|
46
|
+
tool,
|
|
47
|
+
embed,
|
|
48
|
+
embedMany,
|
|
49
|
+
smoothStream,
|
|
50
|
+
} from 'ai'
|
|
51
|
+
export { openai, createOpenAI } from '@ai-sdk/openai'
|
|
52
|
+
export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts'
|
|
53
|
+
export type { PostgresOptions, PostgresClient, PostgresInjected } from './postgres/types.ts'
|
|
54
|
+
export { redis } from './redis/index.ts'
|
|
55
|
+
export type { RedisOptions, RedisClient, RedisInjected } from './redis/types.ts'
|
|
56
|
+
export { createHub } from './hub.ts'
|
|
57
|
+
export type { Hub, HubOptions } from './hub.ts'
|
|
58
|
+
export { queue } from './queue/index.ts'
|
|
59
|
+
export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types.ts'
|
|
60
|
+
export { health } from './middleware/health.ts'
|
|
61
|
+
export type { HealthOptions } from './middleware/health.ts'
|
|
62
|
+
export { theme } from './middleware/theme.ts'
|
|
63
|
+
export type { ThemeOptions, ThemeInjected } from './middleware/theme.ts'
|
|
64
|
+
export { i18n } from './middleware/i18n.ts'
|
|
65
|
+
export type { I18nOptions, I18nInjected } from './middleware/i18n.ts'
|
|
66
|
+
export { flash } from './middleware/flash.ts'
|
|
67
|
+
export type { FlashOptions, FlashInjected, FlashModule } from './middleware/flash.ts'
|
|
68
|
+
export { mailer } from './mailer.ts'
|
|
69
|
+
export type { MailerOptions, MailOptions, Mailer } from './mailer.ts'
|
|
70
|
+
export { csrf } from './middleware/csrf.ts'
|
|
71
|
+
export type { CsrfOptions, CsrfInjected, CsrfModule } from './middleware/csrf.ts'
|
package/mailer.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createTransport, type Transporter } from 'nodemailer'
|
|
2
|
+
import type { Closeable } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/** Options for sending an email. */
|
|
5
|
+
export interface MailOptions {
|
|
6
|
+
/** Recipient address(es). */
|
|
7
|
+
to: string | string[]
|
|
8
|
+
/** Email subject. */
|
|
9
|
+
subject: string
|
|
10
|
+
/** Plain text body. */
|
|
11
|
+
text?: string
|
|
12
|
+
/** HTML body. */
|
|
13
|
+
html?: string
|
|
14
|
+
/** Sender address (overrides `MailerOptions.from`). */
|
|
15
|
+
from?: string
|
|
16
|
+
/** CC recipient(s). */
|
|
17
|
+
cc?: string | string[]
|
|
18
|
+
/** BCC recipient(s). */
|
|
19
|
+
bcc?: string | string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options for {@link mailer}. */
|
|
23
|
+
export interface MailerOptions {
|
|
24
|
+
/** Nodemailer transport string or pre-built transporter object. */
|
|
25
|
+
transport?: string | Transporter
|
|
26
|
+
/** Default sender address. */
|
|
27
|
+
from?: string
|
|
28
|
+
/** Custom send function (bypasses nodemailer). */
|
|
29
|
+
send?: (opts: MailOptions) => Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Mailer instance returned by {@link mailer}. */
|
|
33
|
+
export interface Mailer extends Closeable {
|
|
34
|
+
/** Send an email. */
|
|
35
|
+
send: (opts: MailOptions) => Promise<void>
|
|
36
|
+
/** Close the nodemailer transport. */
|
|
37
|
+
close: () => Promise<void>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a mailer instance.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { mailer } from 'weifuwu'
|
|
45
|
+
*
|
|
46
|
+
* const email = mailer({ transport: 'smtp://user:pass@smtp.example.com' })
|
|
47
|
+
* await email.send({
|
|
48
|
+
* to: 'user@example.com',
|
|
49
|
+
* subject: 'Hello',
|
|
50
|
+
* text: 'Hello from weifuwu!',
|
|
51
|
+
* })
|
|
52
|
+
* await email.close()
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function mailer(options: MailerOptions): Mailer {
|
|
56
|
+
const sender = options.send
|
|
57
|
+
const from = options.from
|
|
58
|
+
|
|
59
|
+
let transporter: Transporter | null = null
|
|
60
|
+
if (!sender && options.transport) {
|
|
61
|
+
transporter =
|
|
62
|
+
typeof options.transport === 'string' ? createTransport(options.transport) : options.transport
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function send(opts: MailOptions): Promise<void> {
|
|
66
|
+
if (sender) {
|
|
67
|
+
await sender(opts)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (!transporter) {
|
|
71
|
+
throw new Error('mailer: no transport configured — provide `transport` or `send` option')
|
|
72
|
+
}
|
|
73
|
+
await transporter.sendMail({ ...opts, from: opts.from ?? from })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function close(): Promise<void> {
|
|
77
|
+
transporter?.close()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { send, close }
|
|
81
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { constants, brotliCompress, gzip, deflate } from 'node:zlib'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import type { Middleware } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
const brotliCompressAsync = promisify(brotliCompress)
|
|
6
|
+
const gzipAsync = promisify(gzip)
|
|
7
|
+
const deflateAsync = promisify(deflate)
|
|
8
|
+
|
|
9
|
+
/** Options for {@link compress}. */
|
|
10
|
+
export interface CompressOptions {
|
|
11
|
+
/** Compression level (1-9, default: 6). */
|
|
12
|
+
level?: number
|
|
13
|
+
/** Minimum response body size in bytes to compress (default: 1024). */
|
|
14
|
+
threshold?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Response compression middleware (brotli, gzip, deflate).
|
|
19
|
+
*
|
|
20
|
+
* Automatically selects the best encoding based on `Accept-Encoding` header.
|
|
21
|
+
* Skips compression for small responses, images, audio, video, and already-encoded responses.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { compress } from 'weifuwu'
|
|
25
|
+
* app.use(compress())
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function compress(options?: CompressOptions): Middleware {
|
|
29
|
+
const level = options?.level ?? 6
|
|
30
|
+
const threshold = options?.threshold ?? 1024
|
|
31
|
+
|
|
32
|
+
return async (req, ctx, next) => {
|
|
33
|
+
const accept = req.headers.get('accept-encoding') ?? ''
|
|
34
|
+
|
|
35
|
+
const encoding = accept.includes('br')
|
|
36
|
+
? 'br'
|
|
37
|
+
: accept.includes('gzip')
|
|
38
|
+
? 'gzip'
|
|
39
|
+
: accept.includes('deflate')
|
|
40
|
+
? 'deflate'
|
|
41
|
+
: ''
|
|
42
|
+
|
|
43
|
+
if (!encoding) return next(req, ctx)
|
|
44
|
+
|
|
45
|
+
const res = await next(req, ctx)
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
res.status === 304 ||
|
|
49
|
+
res.status === 204 ||
|
|
50
|
+
res.status === 206 ||
|
|
51
|
+
res.status < 200 ||
|
|
52
|
+
res.status >= 300
|
|
53
|
+
) {
|
|
54
|
+
return res
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (res.headers.get('content-encoding')) return res
|
|
58
|
+
|
|
59
|
+
const ct = res.headers.get('content-type') ?? ''
|
|
60
|
+
if (
|
|
61
|
+
!ct ||
|
|
62
|
+
ct.startsWith('audio/') ||
|
|
63
|
+
ct.startsWith('video/') ||
|
|
64
|
+
ct.startsWith('image/') ||
|
|
65
|
+
ct === 'application/zip'
|
|
66
|
+
) {
|
|
67
|
+
return res
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!res.body) return res
|
|
71
|
+
|
|
72
|
+
const body = await res.bytes()
|
|
73
|
+
if (body.byteLength < threshold) return res
|
|
74
|
+
|
|
75
|
+
let compressed: Buffer
|
|
76
|
+
try {
|
|
77
|
+
if (encoding === 'br') {
|
|
78
|
+
compressed = await brotliCompressAsync(body, {
|
|
79
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) },
|
|
80
|
+
})
|
|
81
|
+
} else if (encoding === 'gzip') {
|
|
82
|
+
compressed = await gzipAsync(body, { level: Math.min(level, 9) })
|
|
83
|
+
} else {
|
|
84
|
+
compressed = await deflateAsync(body, { level: Math.min(level, 9) })
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
return res
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const headers = new Headers(res.headers)
|
|
91
|
+
headers.set('Content-Encoding', encoding)
|
|
92
|
+
headers.set('Content-Length', String(compressed.byteLength))
|
|
93
|
+
headers.delete('Content-Range')
|
|
94
|
+
const existingVary = headers.get('Vary')
|
|
95
|
+
headers.set('Vary', existingVary ? `${existingVary}, Accept-Encoding` : 'Accept-Encoding')
|
|
96
|
+
|
|
97
|
+
return new Response(compressed as BodyInit, {
|
|
98
|
+
status: res.status,
|
|
99
|
+
statusText: res.statusText,
|
|
100
|
+
headers,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|