silgi 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aws-lambda.mjs +1 -1
- package/dist/broker/redis.mjs +1 -2
- package/dist/builder.mjs +35 -7
- package/dist/caller.mjs +67 -50
- package/dist/client/plugins/retry.mjs +9 -4
- package/dist/compile.d.mts +17 -10
- package/dist/compile.mjs +161 -144
- package/dist/core/ctx-symbols.mjs +18 -1
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +94 -83
- package/dist/core/input.mjs +116 -37
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +36 -8
- package/dist/core/task.mjs +210 -90
- package/dist/core/url.mjs +19 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/integrations/better-auth/index.mjs +11 -2
- package/dist/lazy.mjs +3 -0
- package/dist/map-input.d.mts +8 -6
- package/dist/map-input.mjs +8 -6
- package/dist/plugins/analytics/routes.mjs +25 -13
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -134
- package/dist/plugins/coerce.d.mts +3 -2
- package/dist/plugins/coerce.mjs +25 -8
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.d.mts +35 -0
- package/dist/silgi.mjs +177 -103
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +128 -89
- package/package.json +2 -4
|
@@ -48,6 +48,14 @@ function runWithCtx(ctx, fn) {
|
|
|
48
48
|
* Keyed on `Request` identity. GC-friendly: entry auto-released when the
|
|
49
49
|
* `Request` is collected.
|
|
50
50
|
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* This is a documented exception to ARCHITECTURE §3 ("no WeakMap keyed on
|
|
53
|
+
* Request identity"). Kept only because `setRequestContext` is a public
|
|
54
|
+
* API consumed by user code outside silgi's control — removing it is a
|
|
55
|
+
* breaking change reserved for the next major. New integrations must
|
|
56
|
+
* use `silgi.runInContext(ctx, fn)` or the per-plugin closure pattern
|
|
57
|
+
* (see `requestMetas` inside `tracing()` below).
|
|
58
|
+
*
|
|
51
59
|
* @internal
|
|
52
60
|
*/
|
|
53
61
|
const _requestContextMap = /* @__PURE__ */ new WeakMap();
|
|
@@ -186,7 +194,6 @@ function extractUserData(returned) {
|
|
|
186
194
|
}
|
|
187
195
|
return result;
|
|
188
196
|
}
|
|
189
|
-
const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
190
197
|
/**
|
|
191
198
|
* Creates a Better Auth plugin that auto-traces all auth operations
|
|
192
199
|
* into silgi analytics spans.
|
|
@@ -197,6 +204,8 @@ const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
|
197
204
|
function tracing(config) {
|
|
198
205
|
const captureInput = config?.captureInput ?? true;
|
|
199
206
|
const captureOutput = config?.captureOutput ?? true;
|
|
207
|
+
const wrapMiddleware = config?.createAuthMiddleware ?? ((fn) => fn);
|
|
208
|
+
const requestMetas = /* @__PURE__ */ new WeakMap();
|
|
200
209
|
return {
|
|
201
210
|
id: "silgi-tracing",
|
|
202
211
|
onRequest: async (request, _ctx) => {
|
|
@@ -215,7 +224,7 @@ function tracing(config) {
|
|
|
215
224
|
},
|
|
216
225
|
hooks: { after: [{
|
|
217
226
|
matcher: () => true,
|
|
218
|
-
handler: (
|
|
227
|
+
handler: wrapMiddleware(async (ctx) => {
|
|
219
228
|
try {
|
|
220
229
|
const request = ctx.request;
|
|
221
230
|
if (!request) return;
|
package/dist/lazy.mjs
CHANGED
package/dist/map-input.d.mts
CHANGED
|
@@ -4,13 +4,15 @@ import { WrapDef } from "./types.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* Create a wrap that transforms the procedure input before execution.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The mapper receives the raw input and returns the transformed input.
|
|
9
|
+
* Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
|
|
10
|
+
* symbol slot on `ctx`; the resolver reads the same slot to get the
|
|
11
|
+
* rewritten input. Users never interact with the slot directly — it's
|
|
12
|
+
* framework-internal (see `src/core/ctx-symbols.ts`).
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* before calling next().
|
|
14
|
+
* Must run inside the wrap onion (not as a guard) so it can observe and
|
|
15
|
+
* mutate the already-parsed input after schema validation.
|
|
14
16
|
*/
|
|
15
17
|
declare function mapInput<TIn = unknown, TOut = unknown>(mapper: (input: TIn) => TOut | Promise<TOut>): WrapDef;
|
|
16
18
|
//#endregion
|
package/dist/map-input.mjs
CHANGED
|
@@ -25,13 +25,15 @@ import { RAW_INPUT } from "./core/ctx-symbols.mjs";
|
|
|
25
25
|
/**
|
|
26
26
|
* Create a wrap that transforms the procedure input before execution.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* The mapper receives the raw input and returns the transformed input.
|
|
30
|
+
* Internally this wrap replaces the value in the pipeline's `RAW_INPUT`
|
|
31
|
+
* symbol slot on `ctx`; the resolver reads the same slot to get the
|
|
32
|
+
* rewritten input. Users never interact with the slot directly — it's
|
|
33
|
+
* framework-internal (see `src/core/ctx-symbols.ts`).
|
|
31
34
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* before calling next().
|
|
35
|
+
* Must run inside the wrap onion (not as a guard) so it can observe and
|
|
36
|
+
* mutate the already-parsed input after schema validation.
|
|
35
37
|
*/
|
|
36
38
|
function mapInput(mapper) {
|
|
37
39
|
return {
|
|
@@ -107,6 +107,22 @@ function parseAnalyticsDetailPath(pathname, prefix) {
|
|
|
107
107
|
function jsonResponse(data, headers) {
|
|
108
108
|
return new Response(JSON.stringify(data), { headers });
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse a request body as JSON without throwing. Returns `null` on any
|
|
112
|
+
* failure (empty body, malformed JSON, non-JSON content). Used by the
|
|
113
|
+
* analytics hidden-paths endpoint so a bad request produces a 400 instead
|
|
114
|
+
* of an uncaught exception that surfaces as a 500 to the client.
|
|
115
|
+
*/
|
|
116
|
+
async function parseJsonBody(request) {
|
|
117
|
+
try {
|
|
118
|
+
const text = await request.text();
|
|
119
|
+
if (!text) return null;
|
|
120
|
+
const parsed = JSON.parse(text);
|
|
121
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
110
126
|
/** Serve analytics dashboard and API routes. */
|
|
111
127
|
async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml) {
|
|
112
128
|
const jsonCacheHeaders = {
|
|
@@ -122,24 +138,20 @@ async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml)
|
|
|
122
138
|
if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
|
|
123
139
|
if (pathname === "api/analytics/hidden") {
|
|
124
140
|
if (request.method === "GET") return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
125
|
-
if (request.method === "POST") {
|
|
126
|
-
const body = await request
|
|
127
|
-
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
128
|
-
status: 400,
|
|
129
|
-
headers: jsonCacheHeaders
|
|
130
|
-
});
|
|
131
|
-
collector.addHiddenPath(body.path);
|
|
132
|
-
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
133
|
-
}
|
|
134
|
-
if (request.method === "DELETE") {
|
|
135
|
-
const body = await request.json();
|
|
136
|
-
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
141
|
+
if (request.method === "POST" || request.method === "DELETE") {
|
|
142
|
+
const body = await parseJsonBody(request);
|
|
143
|
+
if (!body || typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
137
144
|
status: 400,
|
|
138
145
|
headers: jsonCacheHeaders
|
|
139
146
|
});
|
|
140
|
-
collector.
|
|
147
|
+
if (request.method === "POST") collector.addHiddenPath(body.path);
|
|
148
|
+
else collector.removeHiddenPath(body.path);
|
|
141
149
|
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
142
150
|
}
|
|
151
|
+
return new Response("{\"error\":\"method not allowed\"}", {
|
|
152
|
+
status: 405,
|
|
153
|
+
headers: jsonCacheHeaders
|
|
154
|
+
});
|
|
143
155
|
}
|
|
144
156
|
if (pathname === "api/analytics/errors") return jsonResponse(queryErrors((await collector.getErrors()).filter((e) => !collector.isHidden(e.procedure)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
|
145
157
|
if (pathname === "api/analytics/requests") return jsonResponse(queryRequests((await collector.getRequests()).filter((r) => !collector.isHidden(r.path)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
package/dist/plugins/cache.d.mts
CHANGED
|
@@ -2,169 +2,105 @@ import { WrapDef } from "../types.mjs";
|
|
|
2
2
|
import { CacheEntry, CacheOptions, StorageInterface } from "ocache";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/cache.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimal interface matching an unstorage `Storage`. We describe it
|
|
7
|
+
* locally so the cache plugin does not take a hard dependency on the
|
|
8
|
+
* unstorage package — users bring their own.
|
|
9
|
+
*/
|
|
10
|
+
interface UnstorageCompatible {
|
|
11
|
+
getItem<T = unknown>(key: string): Promise<T | null> | T | null;
|
|
12
|
+
setItem(key: string, value: unknown, opts?: {
|
|
13
|
+
ttl?: number;
|
|
14
|
+
}): Promise<void> | void;
|
|
15
|
+
removeItem(key: string): Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Public helper for users who already have an unstorage instance.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const storage = createStorage({ driver: redisDriver({ ... }) })
|
|
22
|
+
* setCacheStorage(createUnstorageAdapter(storage))
|
|
23
|
+
*/
|
|
24
|
+
declare function createUnstorageAdapter(storage: UnstorageCompatible): StorageInterface;
|
|
5
25
|
interface CacheQueryOptions<T = unknown> {
|
|
6
|
-
/** Cache TTL in seconds
|
|
26
|
+
/** Cache TTL in seconds. @default 60 */
|
|
7
27
|
maxAge?: number;
|
|
8
|
-
/** Enable stale-while-revalidate
|
|
28
|
+
/** Enable stale-while-revalidate. @default true */
|
|
9
29
|
swr?: boolean;
|
|
10
|
-
/** Max seconds to serve stale while revalidating
|
|
30
|
+
/** Max seconds to serve stale while revalidating. @default maxAge */
|
|
11
31
|
staleMaxAge?: number;
|
|
12
|
-
/** Custom cache
|
|
32
|
+
/** Custom cache-key generator from the request input. */
|
|
13
33
|
getKey?: (input: unknown) => string;
|
|
14
|
-
/**
|
|
34
|
+
/** Human-readable cache name. Defaults to the procedure path. */
|
|
15
35
|
name?: string;
|
|
16
36
|
/**
|
|
17
|
-
*
|
|
18
|
-
* Useful for admin users or debug modes.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* cacheQuery({
|
|
23
|
-
* shouldBypassCache: (input) => input?.noCache === true,
|
|
24
|
-
* })
|
|
25
|
-
* ```
|
|
37
|
+
* Return `true` to skip cache entirely and call the resolver
|
|
38
|
+
* directly. Useful for admin users or debug modes.
|
|
26
39
|
*/
|
|
27
40
|
shouldBypassCache?: (input: unknown) => boolean | Promise<boolean>;
|
|
28
41
|
/**
|
|
29
|
-
*
|
|
42
|
+
* Return `true` to invalidate cache for this key and re-resolve.
|
|
30
43
|
* The new result is cached normally.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* cacheQuery({
|
|
35
|
-
* shouldInvalidateCache: (input) => input?.refresh === true,
|
|
36
|
-
* })
|
|
37
|
-
* ```
|
|
38
44
|
*/
|
|
39
45
|
shouldInvalidateCache?: (input: unknown) => boolean | Promise<boolean>;
|
|
40
|
-
/**
|
|
41
|
-
* Validate a cache entry before returning it.
|
|
42
|
-
* Return `false` to treat as cache miss and re-resolve.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```ts
|
|
46
|
-
* cacheQuery({
|
|
47
|
-
* validate: (entry) => entry.value !== null && entry.value !== undefined,
|
|
48
|
-
* })
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
46
|
+
/** Validate a cache entry before returning it. `false` = treat as miss. */
|
|
51
47
|
validate?: (entry: CacheEntry<T>) => boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Transform a cache entry before returning.
|
|
54
|
-
* Runs on both cache hits and fresh resolves.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```ts
|
|
58
|
-
* cacheQuery({
|
|
59
|
-
* transform: (entry) => ({ ...entry.value, cachedAt: entry.mtime }),
|
|
60
|
-
* })
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
48
|
+
/** Transform a cache entry before returning. Runs on both hits and fresh resolves. */
|
|
63
49
|
transform?: (entry: CacheEntry<T>) => T;
|
|
64
|
-
/**
|
|
65
|
-
* Storage base prefix for cache keys.
|
|
66
|
-
* Defaults to `'/cache'`.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```ts
|
|
70
|
-
* cacheQuery({
|
|
71
|
-
* base: '/my-app-cache',
|
|
72
|
-
* })
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
50
|
+
/** Storage key prefix. @default '/cache' */
|
|
75
51
|
base?: string;
|
|
76
52
|
/**
|
|
77
|
-
* Custom integrity value. Auto-generated from the resolver + options
|
|
78
|
-
*
|
|
53
|
+
* Custom integrity value. Auto-generated from the resolver + options
|
|
54
|
+
* by default; when it changes (e.g. after a redeploy) stale cache is
|
|
55
|
+
* invalidated.
|
|
79
56
|
*/
|
|
80
57
|
integrity?: string;
|
|
81
|
-
/** Error handler for cache read/write/SWR failures */
|
|
58
|
+
/** Error handler for cache read / write / SWR failures. */
|
|
82
59
|
onError?: (error: unknown) => void;
|
|
83
60
|
}
|
|
84
61
|
/**
|
|
85
|
-
* Wrap middleware that caches query
|
|
62
|
+
* Wrap middleware that caches a query procedure's output.
|
|
86
63
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
64
|
+
* Defaults: 60-second TTL, SWR on. Every other knob comes from
|
|
65
|
+
* {@link CacheQueryOptions}.
|
|
89
66
|
*
|
|
90
67
|
* @example
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* swr: true,
|
|
101
|
-
* staleMaxAge: 600,
|
|
102
|
-
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
103
|
-
* shouldInvalidateCache: (input) => (input as any)?.refresh,
|
|
104
|
-
* validate: (entry) => Array.isArray(entry.value),
|
|
105
|
-
* onError: (err) => console.error('[cache]', err),
|
|
106
|
-
* }))
|
|
107
|
-
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
108
|
-
* ```
|
|
68
|
+
* const listPosts = k
|
|
69
|
+
* .$use(cacheQuery({
|
|
70
|
+
* maxAge: 300,
|
|
71
|
+
* staleMaxAge: 600,
|
|
72
|
+
* shouldBypassCache: (input) => (input as any)?.noCache,
|
|
73
|
+
* validate: (entry) => Array.isArray(entry.value),
|
|
74
|
+
* onError: (err) => console.error('[cache]', err),
|
|
75
|
+
* }))
|
|
76
|
+
* .$resolve(({ ctx }) => ctx.db.posts.findMany())
|
|
109
77
|
*/
|
|
110
78
|
declare function cacheQuery<T = unknown>(options?: CacheQueryOptions<T>): WrapDef;
|
|
111
79
|
/**
|
|
112
|
-
*
|
|
80
|
+
* Wipe every cached entry for a procedure by its cache name.
|
|
113
81
|
*
|
|
114
|
-
*
|
|
82
|
+
* Typically called after a mutation that makes related queries stale.
|
|
83
|
+
* Names default to the procedure's auto-generated path, or whatever
|
|
84
|
+
* was passed via `cacheQuery({ name })`.
|
|
115
85
|
*
|
|
116
86
|
* @example
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* })
|
|
123
|
-
* ```
|
|
87
|
+
* const createUser = k.$resolve(async ({ input, ctx }) => {
|
|
88
|
+
* const user = await ctx.db.users.create(input)
|
|
89
|
+
* await invalidateQueryCache('users_list')
|
|
90
|
+
* return user
|
|
91
|
+
* })
|
|
124
92
|
*/
|
|
125
93
|
declare function invalidateQueryCache(name: string): Promise<void>;
|
|
126
94
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* For production, use `createUnstorageAdapter()` with Redis, Cloudflare KV, etc.
|
|
95
|
+
* Replace ocache's storage backend. Default is an in-memory map with
|
|
96
|
+
* TTL; production deployments usually want Redis or Cloudflare KV via
|
|
97
|
+
* `createUnstorageAdapter()`.
|
|
131
98
|
*
|
|
132
99
|
* @example
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
137
|
-
*
|
|
138
|
-
* setCacheStorage(createUnstorageAdapter(
|
|
139
|
-
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
140
|
-
* ))
|
|
141
|
-
* ```
|
|
100
|
+
* setCacheStorage(createUnstorageAdapter(
|
|
101
|
+
* createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
102
|
+
* ))
|
|
142
103
|
*/
|
|
143
104
|
declare function setCacheStorage(storage: StorageInterface): void;
|
|
144
|
-
/**
|
|
145
|
-
* Minimal interface matching unstorage's Storage.
|
|
146
|
-
* Avoids hard dependency on unstorage — users bring their own.
|
|
147
|
-
*/
|
|
148
|
-
interface UnstorageCompatible {
|
|
149
|
-
getItem<T = unknown>(key: string): Promise<T | null> | T | null;
|
|
150
|
-
setItem(key: string, value: unknown, opts?: {
|
|
151
|
-
ttl?: number;
|
|
152
|
-
}): Promise<void> | void;
|
|
153
|
-
removeItem(key: string): Promise<void> | void;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Create an ocache-compatible storage adapter from an unstorage instance.
|
|
157
|
-
*
|
|
158
|
-
* @example
|
|
159
|
-
* ```ts
|
|
160
|
-
* import { createStorage } from 'silgi/unstorage'
|
|
161
|
-
* import redisDriver from 'unstorage/drivers/redis'
|
|
162
|
-
*
|
|
163
|
-
* const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
|
|
164
|
-
* const adapter = createUnstorageAdapter(storage)
|
|
165
|
-
* setCacheStorage(adapter)
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
|
-
declare function createUnstorageAdapter(storage: UnstorageCompatible): StorageInterface;
|
|
169
105
|
//#endregion
|
|
170
106
|
export { type CacheEntry, type CacheOptions, CacheQueryOptions, type StorageInterface, UnstorageCompatible, cacheQuery, createUnstorageAdapter, invalidateQueryCache, setCacheStorage };
|