silgi 0.0.13 → 0.1.0-beta.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.
Files changed (186) hide show
  1. package/README.md +102 -1
  2. package/dist/_virtual/_rolldown/runtime.mjs +5 -0
  3. package/dist/adapters/astro.d.mts +17 -0
  4. package/dist/adapters/astro.mjs +24 -0
  5. package/dist/adapters/aws-lambda.d.mts +31 -0
  6. package/dist/adapters/aws-lambda.mjs +85 -0
  7. package/dist/adapters/elysia.d.mts +17 -0
  8. package/dist/adapters/elysia.mjs +76 -0
  9. package/dist/adapters/express.d.mts +16 -0
  10. package/dist/adapters/express.mjs +78 -0
  11. package/dist/adapters/fastify.d.mts +15 -0
  12. package/dist/adapters/fastify.mjs +78 -0
  13. package/dist/adapters/message-port.d.mts +37 -0
  14. package/dist/adapters/message-port.mjs +129 -0
  15. package/dist/adapters/nestjs.d.mts +25 -0
  16. package/dist/adapters/nestjs.mjs +91 -0
  17. package/dist/adapters/nextjs.d.mts +21 -0
  18. package/dist/adapters/nextjs.mjs +30 -0
  19. package/dist/adapters/peer.d.mts +27 -0
  20. package/dist/adapters/peer.mjs +36 -0
  21. package/dist/adapters/remix.d.mts +17 -0
  22. package/dist/adapters/remix.mjs +24 -0
  23. package/dist/adapters/solidstart.d.mts +14 -0
  24. package/dist/adapters/solidstart.mjs +30 -0
  25. package/dist/adapters/sveltekit.d.mts +18 -0
  26. package/dist/adapters/sveltekit.mjs +33 -0
  27. package/dist/analyze.mjs +26 -0
  28. package/dist/broker/index.d.mts +62 -0
  29. package/dist/broker/index.mjs +153 -0
  30. package/dist/broker/nats.d.mts +33 -0
  31. package/dist/broker/nats.mjs +31 -0
  32. package/dist/broker/redis.d.mts +51 -0
  33. package/dist/broker/redis.mjs +92 -0
  34. package/dist/builder.d.mts +36 -0
  35. package/dist/builder.mjs +51 -0
  36. package/dist/callable.d.mts +17 -0
  37. package/dist/callable.mjs +42 -0
  38. package/dist/client/adapters/fetch/index.d.mts +17 -0
  39. package/dist/client/adapters/fetch/index.mjs +61 -0
  40. package/dist/client/adapters/ofetch/index.d.mts +41 -0
  41. package/dist/client/adapters/ofetch/index.mjs +92 -0
  42. package/dist/client/client.d.mts +29 -0
  43. package/dist/client/client.mjs +54 -0
  44. package/dist/client/dynamic-link.d.mts +15 -0
  45. package/dist/client/dynamic-link.mjs +16 -0
  46. package/dist/client/index.d.mts +7 -0
  47. package/dist/client/index.mjs +6 -0
  48. package/dist/client/interceptor.d.mts +31 -0
  49. package/dist/client/interceptor.mjs +34 -0
  50. package/dist/client/merge.d.mts +28 -0
  51. package/dist/client/merge.mjs +30 -0
  52. package/dist/client/openapi.d.mts +29 -0
  53. package/dist/client/openapi.mjs +89 -0
  54. package/dist/client/plugins/batch.d.mts +20 -0
  55. package/dist/client/plugins/batch.mjs +64 -0
  56. package/dist/client/plugins/csrf.d.mts +13 -0
  57. package/dist/client/plugins/csrf.mjs +20 -0
  58. package/dist/client/plugins/dedupe.d.mts +10 -0
  59. package/dist/client/plugins/dedupe.mjs +28 -0
  60. package/dist/client/plugins/index.d.mts +5 -0
  61. package/dist/client/plugins/index.mjs +5 -0
  62. package/dist/client/plugins/retry.d.mts +11 -0
  63. package/dist/client/plugins/retry.mjs +21 -0
  64. package/dist/client/server.d.mts +16 -0
  65. package/dist/client/server.mjs +60 -0
  66. package/dist/client/types.d.mts +29 -0
  67. package/dist/codec/devalue.d.mts +21 -0
  68. package/dist/codec/devalue.mjs +32 -0
  69. package/dist/codec/msgpack.d.mts +21 -0
  70. package/dist/codec/msgpack.mjs +59 -0
  71. package/dist/compile.d.mts +52 -0
  72. package/dist/compile.mjs +304 -0
  73. package/dist/contract.d.mts +36 -0
  74. package/dist/contract.mjs +40 -0
  75. package/dist/core/error.d.mts +104 -0
  76. package/dist/core/error.mjs +139 -0
  77. package/dist/core/handler.mjs +546 -0
  78. package/dist/core/iterator.d.mts +17 -0
  79. package/dist/core/iterator.mjs +79 -0
  80. package/dist/core/router-utils.mjs +16 -0
  81. package/dist/core/schema.d.mts +19 -0
  82. package/dist/core/schema.mjs +26 -0
  83. package/dist/core/serve.mjs +38 -0
  84. package/dist/core/sse.d.mts +16 -0
  85. package/dist/core/sse.mjs +95 -0
  86. package/dist/core/storage.d.mts +21 -0
  87. package/dist/core/storage.mjs +63 -0
  88. package/dist/core/utils.mjs +21 -0
  89. package/dist/fast-stringify.mjs +125 -0
  90. package/dist/index.d.mts +15 -37
  91. package/dist/index.mjs +13 -7
  92. package/dist/integrations/ai/index.d.mts +25 -0
  93. package/dist/integrations/ai/index.mjs +116 -0
  94. package/dist/integrations/react/index.d.mts +83 -0
  95. package/dist/integrations/react/index.mjs +197 -0
  96. package/dist/integrations/tanstack-query/index.d.mts +120 -0
  97. package/dist/integrations/tanstack-query/index.mjs +100 -0
  98. package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
  99. package/dist/integrations/tanstack-query/ssr.mjs +89 -0
  100. package/dist/integrations/zod/converter.d.mts +75 -0
  101. package/dist/integrations/zod/converter.mjs +345 -0
  102. package/dist/integrations/zod/index.d.mts +2 -0
  103. package/dist/integrations/zod/index.mjs +2 -0
  104. package/dist/lazy.d.mts +24 -0
  105. package/dist/lazy.mjs +27 -0
  106. package/dist/lifecycle.d.mts +36 -0
  107. package/dist/lifecycle.mjs +46 -0
  108. package/dist/map-input.d.mts +17 -0
  109. package/dist/map-input.mjs +24 -0
  110. package/dist/plugins/analytics.d.mts +168 -0
  111. package/dist/plugins/analytics.mjs +459 -0
  112. package/dist/plugins/batch-server.d.mts +20 -0
  113. package/dist/plugins/batch-server.mjs +86 -0
  114. package/dist/plugins/body-limit.d.mts +16 -0
  115. package/dist/plugins/body-limit.mjs +44 -0
  116. package/dist/plugins/cache.d.mts +170 -0
  117. package/dist/plugins/cache.mjs +200 -0
  118. package/dist/plugins/coerce.d.mts +21 -0
  119. package/dist/plugins/coerce.mjs +46 -0
  120. package/dist/plugins/compression.d.mts +19 -0
  121. package/dist/plugins/compression.mjs +23 -0
  122. package/dist/plugins/cookies.d.mts +44 -0
  123. package/dist/plugins/cookies.mjs +67 -0
  124. package/dist/plugins/cors.d.mts +39 -0
  125. package/dist/plugins/cors.mjs +56 -0
  126. package/dist/plugins/custom-serializer.d.mts +57 -0
  127. package/dist/plugins/custom-serializer.mjs +40 -0
  128. package/dist/plugins/file-upload.d.mts +38 -0
  129. package/dist/plugins/file-upload.mjs +100 -0
  130. package/dist/plugins/index.d.mts +16 -0
  131. package/dist/plugins/index.mjs +16 -0
  132. package/dist/plugins/otel.d.mts +35 -0
  133. package/dist/plugins/otel.mjs +40 -0
  134. package/dist/plugins/pino.d.mts +60 -0
  135. package/dist/plugins/pino.mjs +42 -0
  136. package/dist/plugins/pubsub.d.mts +50 -0
  137. package/dist/plugins/pubsub.mjs +53 -0
  138. package/dist/plugins/ratelimit.d.mts +51 -0
  139. package/dist/plugins/ratelimit.mjs +81 -0
  140. package/dist/plugins/signing.d.mts +41 -0
  141. package/dist/plugins/signing.mjs +115 -0
  142. package/dist/plugins/strict-get.d.mts +10 -0
  143. package/dist/plugins/strict-get.mjs +33 -0
  144. package/dist/route/add.mjs +240 -0
  145. package/dist/route/compiler.mjs +373 -0
  146. package/dist/route/context.mjs +12 -0
  147. package/dist/route/types.d.mts +11 -0
  148. package/dist/route/utils.mjs +17 -0
  149. package/dist/scalar.d.mts +53 -0
  150. package/dist/scalar.mjs +315 -0
  151. package/dist/silgi.d.mts +139 -0
  152. package/dist/silgi.mjs +113 -0
  153. package/dist/trpc-interop.d.mts +22 -0
  154. package/dist/trpc-interop.mjs +68 -0
  155. package/dist/types.d.mts +82 -0
  156. package/dist/ws.d.mts +42 -0
  157. package/dist/ws.mjs +137 -0
  158. package/lib/dashboard/index.html +123 -0
  159. package/lib/ocache.d.mts +1 -0
  160. package/lib/ocache.mjs +1 -0
  161. package/lib/ofetch.d.mts +1 -0
  162. package/lib/ofetch.mjs +1 -0
  163. package/lib/srvx.d.mts +1 -0
  164. package/lib/srvx.mjs +1 -0
  165. package/lib/unstorage.d.mts +1 -0
  166. package/lib/unstorage.mjs +1 -0
  167. package/package.json +291 -65
  168. package/bin/silgi.mjs +0 -3
  169. package/dist/chunks/generate.mjs +0 -933
  170. package/dist/chunks/init.mjs +0 -21
  171. package/dist/cli/config.d.mts +0 -19
  172. package/dist/cli/config.d.ts +0 -19
  173. package/dist/cli/config.mjs +0 -5
  174. package/dist/cli/index.d.mts +0 -2
  175. package/dist/cli/index.d.ts +0 -2
  176. package/dist/cli/index.mjs +0 -119
  177. package/dist/index.d.ts +0 -37
  178. package/dist/plugins/openapi.d.mts +0 -138
  179. package/dist/plugins/openapi.d.ts +0 -138
  180. package/dist/plugins/openapi.mjs +0 -204
  181. package/dist/plugins/scalar.d.mts +0 -14
  182. package/dist/plugins/scalar.d.ts +0 -14
  183. package/dist/plugins/scalar.mjs +0 -66
  184. package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
  185. package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
  186. package/dist/shared/silgi.D5qK9QOm.d.ts +0 -301
@@ -0,0 +1,86 @@
1
+ import { SilgiError, toSilgiError } from "../core/error.mjs";
2
+ import { ValidationError } from "../core/schema.mjs";
3
+ import { compileRouter } from "../compile.mjs";
4
+ //#region src/plugins/batch-server.ts
5
+ /**
6
+ * Server-side batch endpoint — handle multiple RPC calls in one HTTP request.
7
+ *
8
+ * Works with the client-side BatchLink to combine multiple calls into
9
+ * a single HTTP round-trip.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { createBatchHandler } from "silgi/plugins"
14
+ *
15
+ * const batchHandler = createBatchHandler(appRouter, {
16
+ * context: (req) => ({ db: getDB() }),
17
+ * maxBatchSize: 20,
18
+ * })
19
+ *
20
+ * // Mount at /batch endpoint alongside your normal handler
21
+ * ```
22
+ */
23
+ /**
24
+ * Create a Fetch API handler that processes batched RPC calls.
25
+ *
26
+ * Expects a POST with JSON body: `[{ path, input }, ...]`
27
+ * Returns: `[{ data } | { error }, ...]`
28
+ *
29
+ * All calls in a batch share the same context (computed once).
30
+ */
31
+ function createBatchHandler(router, options) {
32
+ const flatRouter = compileRouter(router);
33
+ const { maxBatchSize = 50 } = options;
34
+ return async (request) => {
35
+ if (request.method !== "POST") return Response.json({
36
+ code: "METHOD_NOT_ALLOWED",
37
+ status: 405
38
+ }, { status: 405 });
39
+ let calls;
40
+ try {
41
+ calls = await request.json();
42
+ } catch {
43
+ return Response.json({
44
+ code: "BAD_REQUEST",
45
+ status: 400,
46
+ message: "Invalid JSON"
47
+ }, { status: 400 });
48
+ }
49
+ if (!Array.isArray(calls)) return Response.json({
50
+ code: "BAD_REQUEST",
51
+ status: 400,
52
+ message: "Expected array"
53
+ }, { status: 400 });
54
+ if (calls.length > maxBatchSize) return Response.json({
55
+ code: "BAD_REQUEST",
56
+ status: 400,
57
+ message: `Batch too large: ${calls.length} calls (max ${maxBatchSize})`
58
+ }, { status: 400 });
59
+ const baseCtx = await options.context(request);
60
+ const results = await Promise.all(calls.map(async (call) => {
61
+ const route = flatRouter("POST", "/" + call.path)?.data;
62
+ if (!route) return { error: {
63
+ code: "NOT_FOUND",
64
+ status: 404,
65
+ message: "Procedure not found"
66
+ } };
67
+ try {
68
+ const ctx = Object.create(null);
69
+ const keys = Object.keys(baseCtx);
70
+ for (let i = 0; i < keys.length; i++) ctx[keys[i]] = baseCtx[keys[i]];
71
+ return { data: await route.handler(ctx, call.input, request.signal) };
72
+ } catch (error) {
73
+ if (error instanceof ValidationError) return { error: {
74
+ code: "BAD_REQUEST",
75
+ status: 400,
76
+ message: error.message,
77
+ data: { issues: error.issues }
78
+ } };
79
+ return { error: (error instanceof SilgiError ? error : toSilgiError(error)).toJSON() };
80
+ }
81
+ }));
82
+ return Response.json(results);
83
+ };
84
+ }
85
+ //#endregion
86
+ export { createBatchHandler };
@@ -0,0 +1,16 @@
1
+ import { GuardDef } from "../types.mjs";
2
+
3
+ //#region src/plugins/body-limit.d.ts
4
+ interface BodyLimitOptions {
5
+ /** Maximum body size in bytes. Default: 1_048_576 (1 MB) */
6
+ maxBytes?: number;
7
+ /** Custom error message. */
8
+ message?: string;
9
+ }
10
+ /**
11
+ * Create a guard that rejects requests with bodies larger than `maxBytes`.
12
+ * Checks the Content-Length header — zero overhead for GET requests.
13
+ */
14
+ declare function bodyLimitGuard(options?: BodyLimitOptions): GuardDef<Record<string, unknown>>;
15
+ //#endregion
16
+ export { BodyLimitOptions, bodyLimitGuard };
@@ -0,0 +1,44 @@
1
+ import { SilgiError } from "../core/error.mjs";
2
+ //#region src/plugins/body-limit.ts
3
+ /**
4
+ * Body limit guard — reject oversized request bodies.
5
+ *
6
+ * Throws TOO_LARGE (413) when the Content-Length header exceeds the limit.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { bodyLimitGuard } from "silgi/plugins"
11
+ *
12
+ * const upload = k
13
+ * .$use(bodyLimitGuard({ maxBytes: 5 * 1024 * 1024 })) // 5 MB
14
+ * .$resolve(({ input }) => processUpload(input))
15
+ * ```
16
+ */
17
+ /**
18
+ * Create a guard that rejects requests with bodies larger than `maxBytes`.
19
+ * Checks the Content-Length header — zero overhead for GET requests.
20
+ */
21
+ function bodyLimitGuard(options = {}) {
22
+ const { maxBytes = 1048576, message = "Request body too large" } = options;
23
+ return {
24
+ kind: "guard",
25
+ fn: (ctx) => {
26
+ const headers = ctx.headers;
27
+ if (!headers) return;
28
+ const cl = headers["content-length"];
29
+ if (!cl) return;
30
+ const size = Number.parseInt(cl, 10);
31
+ if (Number.isNaN(size)) return;
32
+ if (size > maxBytes) throw new SilgiError("PAYLOAD_TOO_LARGE", {
33
+ status: 413,
34
+ message,
35
+ data: {
36
+ maxBytes,
37
+ receivedBytes: size
38
+ }
39
+ });
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ export { bodyLimitGuard };
@@ -0,0 +1,170 @@
1
+ import { WrapDef } from "../types.mjs";
2
+ import { CacheEntry, CacheOptions, StorageInterface } from "ocache";
3
+
4
+ //#region src/plugins/cache.d.ts
5
+ interface CacheQueryOptions<T = unknown> {
6
+ /** Cache TTL in seconds (default: 60) */
7
+ maxAge?: number;
8
+ /** Enable stale-while-revalidate (default: true) */
9
+ swr?: boolean;
10
+ /** Max seconds to serve stale while revalidating (default: maxAge) */
11
+ staleMaxAge?: number;
12
+ /** Custom cache key generator from input */
13
+ getKey?: (input: unknown) => string;
14
+ /** Cache key name prefix (default: procedure path, set automatically) */
15
+ name?: string;
16
+ /**
17
+ * When returns `true`, skip cache entirely and call resolver directly.
18
+ * Useful for admin users or debug modes.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * cacheQuery({
23
+ * shouldBypassCache: (input) => input?.noCache === true,
24
+ * })
25
+ * ```
26
+ */
27
+ shouldBypassCache?: (input: unknown) => boolean | Promise<boolean>;
28
+ /**
29
+ * When returns `true`, invalidate cache for this key and re-resolve.
30
+ * The new result is cached normally.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * cacheQuery({
35
+ * shouldInvalidateCache: (input) => input?.refresh === true,
36
+ * })
37
+ * ```
38
+ */
39
+ 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
+ */
51
+ 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
+ */
63
+ 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
+ */
75
+ base?: string;
76
+ /**
77
+ * Custom integrity value. Auto-generated from the resolver + options by default.
78
+ * When integrity changes (e.g. after redeploy), stale cache is invalidated.
79
+ */
80
+ integrity?: string;
81
+ /** Error handler for cache read/write/SWR failures */
82
+ onError?: (error: unknown) => void;
83
+ }
84
+ /**
85
+ * Wrap middleware that caches query results.
86
+ *
87
+ * Uses ocache under the hood: TTL, SWR, dedup, integrity, bypass, invalidation.
88
+ * Default: 60s TTL, SWR enabled.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const listUsers = k
93
+ * .$use(cacheQuery({ maxAge: 60 }))
94
+ * .$resolve(({ ctx }) => ctx.db.users.findMany())
95
+ *
96
+ * // Advanced: bypass cache for admin, custom validation
97
+ * const listPosts = k
98
+ * .$use(cacheQuery({
99
+ * maxAge: 300,
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
+ * ```
109
+ */
110
+ declare function cacheQuery<T = unknown>(options?: CacheQueryOptions<T>): WrapDef;
111
+ /**
112
+ * Invalidate cached entries for a procedure by name.
113
+ *
114
+ * Call this after mutations to clear related query caches.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const createUser = k.$resolve(async ({ input, ctx }) => {
119
+ * const user = await ctx.db.users.create(input)
120
+ * await invalidateQueryCache('users_list')
121
+ * return user
122
+ * })
123
+ * ```
124
+ */
125
+ declare function invalidateQueryCache(name: string): Promise<void>;
126
+ /**
127
+ * Set the cache storage backend.
128
+ *
129
+ * Default: in-memory Map with TTL.
130
+ * For production, use `createUnstorageAdapter()` with Redis, Cloudflare KV, etc.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import { setCacheStorage, createUnstorageAdapter } from 'silgi/cache'
135
+ * import { createStorage } from 'silgi/unstorage'
136
+ * import redisDriver from 'unstorage/drivers/redis'
137
+ *
138
+ * setCacheStorage(createUnstorageAdapter(
139
+ * createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
140
+ * ))
141
+ * ```
142
+ */
143
+ 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
+ //#endregion
170
+ export { type CacheEntry, type CacheOptions, CacheQueryOptions, type StorageInterface, UnstorageCompatible, cacheQuery, createUnstorageAdapter, invalidateQueryCache, setCacheStorage };
@@ -0,0 +1,200 @@
1
+ import { useStorage as useStorage$1 } from "../core/storage.mjs";
2
+ import { defineCachedFunction, setStorage, useStorage } from "ocache";
3
+ import { hash } from "ohash";
4
+ //#region src/plugins/cache.ts
5
+ /**
6
+ * Cache plugin — production-grade response caching powered by ocache.
7
+ *
8
+ * All ocache features exposed:
9
+ * - TTL + Stale-While-Revalidate (SWR)
10
+ * - Request deduplication (concurrent calls share one in-flight promise)
11
+ * - Automatic integrity (redeploy invalidates stale cache)
12
+ * - shouldBypassCache / shouldInvalidateCache callbacks
13
+ * - Entry validation and transformation
14
+ * - Multi-tier storage (read cascade, write to all)
15
+ * - Pluggable storage via `setCacheStorage()` (default: in-memory)
16
+ * - unstorage adapter for Redis, Cloudflare KV, S3, etc.
17
+ * - Mutation-triggered invalidation
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { cacheQuery, setCacheStorage } from 'silgi/cache'
22
+ *
23
+ * // Basic: cache for 60 seconds with SWR
24
+ * const listUsers = k
25
+ * .$use(cacheQuery({ maxAge: 60 }))
26
+ * .$resolve(({ ctx }) => ctx.db.users.findMany())
27
+ *
28
+ * // With unstorage backend (Redis)
29
+ * import { createUnstorageAdapter } from 'silgi/cache'
30
+ * import { createStorage } from 'silgi/unstorage'
31
+ * import redisDriver from 'unstorage/drivers/redis'
32
+ *
33
+ * const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
34
+ * setCacheStorage(createUnstorageAdapter(storage))
35
+ * ```
36
+ */
37
+ /**
38
+ * Auto-connect ocache to silgi's storage.
39
+ * Called lazily on first cacheQuery() use.
40
+ */
41
+ let _storageConnected = false;
42
+ function ensureStorageConnected() {
43
+ if (_storageConnected) return;
44
+ _storageConnected = true;
45
+ try {
46
+ const storage = useStorage$1("cache");
47
+ setStorage({
48
+ get: (key) => storage.getItem(key),
49
+ set: (key, value, opts) => {
50
+ if (value === null || value === void 0) {
51
+ storage.removeItem(key);
52
+ return;
53
+ }
54
+ storage.setItem(key, value, opts?.ttl ? { ttl: opts.ttl } : void 0);
55
+ }
56
+ });
57
+ } catch {}
58
+ }
59
+ /** Registry of cached function keys for invalidation */
60
+ const cacheKeyRegistry = /* @__PURE__ */ new Map();
61
+ /**
62
+ * Wrap middleware that caches query results.
63
+ *
64
+ * Uses ocache under the hood: TTL, SWR, dedup, integrity, bypass, invalidation.
65
+ * Default: 60s TTL, SWR enabled.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const listUsers = k
70
+ * .$use(cacheQuery({ maxAge: 60 }))
71
+ * .$resolve(({ ctx }) => ctx.db.users.findMany())
72
+ *
73
+ * // Advanced: bypass cache for admin, custom validation
74
+ * const listPosts = k
75
+ * .$use(cacheQuery({
76
+ * maxAge: 300,
77
+ * swr: true,
78
+ * staleMaxAge: 600,
79
+ * shouldBypassCache: (input) => (input as any)?.noCache,
80
+ * shouldInvalidateCache: (input) => (input as any)?.refresh,
81
+ * validate: (entry) => Array.isArray(entry.value),
82
+ * onError: (err) => console.error('[cache]', err),
83
+ * }))
84
+ * .$resolve(({ ctx }) => ctx.db.posts.findMany())
85
+ * ```
86
+ */
87
+ function cacheQuery(options = {}) {
88
+ const maxAge = options.maxAge ?? 60;
89
+ const swr = options.swr ?? true;
90
+ const staleMaxAge = options.staleMaxAge ?? maxAge;
91
+ const customGetKey = options.getKey;
92
+ let cacheName = options.name;
93
+ let currentNext = null;
94
+ let cachedFn = null;
95
+ return {
96
+ kind: "wrap",
97
+ fn: async (ctx, next) => {
98
+ ensureStorageConnected();
99
+ if (!cachedFn) {
100
+ if (!cacheName) cacheName = ctx.__procedurePath || `proc_${hash(next.toString()).slice(0, 8)}`;
101
+ const resolvedName = cacheName;
102
+ const keySet = /* @__PURE__ */ new Set();
103
+ cacheKeyRegistry.set(resolvedName, keySet);
104
+ const keyFn = customGetKey ? (input) => customGetKey(input) : (input) => input !== void 0 && input !== null ? hash(input) : "";
105
+ const resolvedBase = options.base ?? "/cache";
106
+ cachedFn = defineCachedFunction(async (_input) => currentNext(), {
107
+ name: resolvedName,
108
+ group: "silgi",
109
+ maxAge,
110
+ swr,
111
+ staleMaxAge,
112
+ base: resolvedBase,
113
+ integrity: options.integrity,
114
+ onError: options.onError,
115
+ validate: options.validate,
116
+ transform: options.transform,
117
+ shouldBypassCache: options.shouldBypassCache ? (input) => options.shouldBypassCache(input) : void 0,
118
+ shouldInvalidateCache: options.shouldInvalidateCache ? (input) => options.shouldInvalidateCache(input) : void 0,
119
+ getKey: (input) => {
120
+ const key = keyFn(input);
121
+ keySet.add(`${resolvedBase}:silgi:${resolvedName}:${key}.json`);
122
+ return key;
123
+ }
124
+ });
125
+ }
126
+ currentNext = next;
127
+ const input = ctx.__rawInput;
128
+ return cachedFn(input);
129
+ }
130
+ };
131
+ }
132
+ /**
133
+ * Invalidate cached entries for a procedure by name.
134
+ *
135
+ * Call this after mutations to clear related query caches.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const createUser = k.$resolve(async ({ input, ctx }) => {
140
+ * const user = await ctx.db.users.create(input)
141
+ * await invalidateQueryCache('users_list')
142
+ * return user
143
+ * })
144
+ * ```
145
+ */
146
+ async function invalidateQueryCache(name) {
147
+ const keys = cacheKeyRegistry.get(name);
148
+ if (keys) {
149
+ const storage = useStorage();
150
+ await Promise.all([...keys].map((key) => storage.set(key, null)));
151
+ keys.clear();
152
+ }
153
+ }
154
+ /**
155
+ * Set the cache storage backend.
156
+ *
157
+ * Default: in-memory Map with TTL.
158
+ * For production, use `createUnstorageAdapter()` with Redis, Cloudflare KV, etc.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { setCacheStorage, createUnstorageAdapter } from 'silgi/cache'
163
+ * import { createStorage } from 'silgi/unstorage'
164
+ * import redisDriver from 'unstorage/drivers/redis'
165
+ *
166
+ * setCacheStorage(createUnstorageAdapter(
167
+ * createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
168
+ * ))
169
+ * ```
170
+ */
171
+ function setCacheStorage(storage) {
172
+ setStorage(storage);
173
+ }
174
+ /**
175
+ * Create an ocache-compatible storage adapter from an unstorage instance.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * import { createStorage } from 'silgi/unstorage'
180
+ * import redisDriver from 'unstorage/drivers/redis'
181
+ *
182
+ * const storage = createStorage({ driver: redisDriver({ url: 'redis://localhost' }) })
183
+ * const adapter = createUnstorageAdapter(storage)
184
+ * setCacheStorage(adapter)
185
+ * ```
186
+ */
187
+ function createUnstorageAdapter(storage) {
188
+ return {
189
+ get: (key) => storage.getItem(key),
190
+ set: (key, value, opts) => {
191
+ if (value === null || value === void 0) {
192
+ storage.removeItem(key);
193
+ return;
194
+ }
195
+ storage.setItem(key, value, opts);
196
+ }
197
+ };
198
+ }
199
+ //#endregion
200
+ export { cacheQuery, createUnstorageAdapter, invalidateQueryCache, setCacheStorage };
@@ -0,0 +1,21 @@
1
+ import { GuardDef } from "../types.mjs";
2
+
3
+ //#region src/plugins/coerce.d.ts
4
+ /**
5
+ * Coerce string values in the input to their proper JavaScript types.
6
+ * Only processes top-level and one-level-deep object values.
7
+ *
8
+ * Rules:
9
+ * - "123", "-42", "3.14" → number
10
+ * - "true", "false" → boolean
11
+ * - "null" → null
12
+ * - "undefined" → undefined
13
+ * - "" → undefined (empty strings become undefined)
14
+ * - Everything else → kept as-is
15
+ */
16
+ declare const coerceGuard: GuardDef<Record<string, unknown>>;
17
+ declare function coerceValue(value: unknown): unknown;
18
+ declare function coerceObject(obj: Record<string, unknown>): void;
19
+ /** Standalone coercion function — use outside of guards */
20
+ //#endregion
21
+ export { coerceGuard, coerceObject, coerceValue };
@@ -0,0 +1,46 @@
1
+ //#region src/plugins/coerce.ts
2
+ /**
3
+ * Coerce string values in the input to their proper JavaScript types.
4
+ * Only processes top-level and one-level-deep object values.
5
+ *
6
+ * Rules:
7
+ * - "123", "-42", "3.14" → number
8
+ * - "true", "false" → boolean
9
+ * - "null" → null
10
+ * - "undefined" → undefined
11
+ * - "" → undefined (empty strings become undefined)
12
+ * - Everything else → kept as-is
13
+ */
14
+ const coerceGuard = {
15
+ kind: "guard",
16
+ fn: (ctx) => {
17
+ const input = ctx.__rawInput;
18
+ if (typeof input !== "object" || input === null) return;
19
+ coerceObject(input);
20
+ }
21
+ };
22
+ function coerceValue(value) {
23
+ if (typeof value !== "string") return value;
24
+ if (value === "") return void 0;
25
+ if (value === "null") return null;
26
+ if (value === "undefined") return void 0;
27
+ if (value === "true") return true;
28
+ if (value === "false") return false;
29
+ if (value.length > 0 && value.length <= 20) {
30
+ const num = Number(value);
31
+ if (!Number.isNaN(num) && String(num) === value) return num;
32
+ }
33
+ return value;
34
+ }
35
+ function coerceObject(obj) {
36
+ const keys = Object.keys(obj);
37
+ for (let i = 0; i < keys.length; i++) {
38
+ const key = keys[i];
39
+ const val = obj[key];
40
+ if (typeof val === "string") obj[key] = coerceValue(val);
41
+ else if (typeof val === "object" && val !== null && !Array.isArray(val)) coerceObject(val);
42
+ else if (Array.isArray(val)) for (let j = 0; j < val.length; j++) val[j] = coerceValue(val[j]);
43
+ }
44
+ }
45
+ //#endregion
46
+ export { coerceGuard, coerceObject, coerceValue };
@@ -0,0 +1,19 @@
1
+ import { WrapDef } from "../types.mjs";
2
+
3
+ //#region src/plugins/compression.d.ts
4
+ interface CompressionOptions {
5
+ /** Minimum response size in bytes before compression kicks in. Default: 1024 */
6
+ threshold?: number;
7
+ /** Preferred encoding. Default: "gzip" */
8
+ encoding?: 'gzip' | 'deflate';
9
+ }
10
+ /**
11
+ * Create a compression wrap middleware.
12
+ *
13
+ * Note: Compression is most useful with handler() / custom servers.
14
+ * With serve(), Node.js handles compression at the HTTP level.
15
+ * This wrap operates on the procedure output before serialization.
16
+ */
17
+ declare function compressionWrap(options?: CompressionOptions): WrapDef;
18
+ //#endregion
19
+ export { CompressionOptions, compressionWrap };
@@ -0,0 +1,23 @@
1
+ //#region src/plugins/compression.ts
2
+ /**
3
+ * Create a compression wrap middleware.
4
+ *
5
+ * Note: Compression is most useful with handler() / custom servers.
6
+ * With serve(), Node.js handles compression at the HTTP level.
7
+ * This wrap operates on the procedure output before serialization.
8
+ */
9
+ function compressionWrap(options = {}) {
10
+ const { threshold = 1024, encoding = "gzip" } = options;
11
+ return {
12
+ kind: "wrap",
13
+ fn: async (_ctx, next) => {
14
+ _ctx.__compression = {
15
+ threshold,
16
+ encoding
17
+ };
18
+ return next();
19
+ }
20
+ };
21
+ }
22
+ //#endregion
23
+ export { compressionWrap };
@@ -0,0 +1,44 @@
1
+ //#region src/plugins/cookies.d.ts
2
+ /**
3
+ * Cookie helpers — parse, set, and delete cookies.
4
+ *
5
+ * Lightweight utilities for working with cookies in Silgi handlers.
6
+ * No dependencies. Works with both serve() and handler().
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { getCookie, setCookie, deleteCookie } from "silgi/cookies"
11
+ *
12
+ * const auth = k.guard((ctx) => {
13
+ * const token = getCookie(ctx.headers, "session")
14
+ * if (!token) throw new SilgiError("UNAUTHORIZED")
15
+ * return { sessionToken: token }
16
+ * })
17
+ * ```
18
+ */
19
+ /** Parse a specific cookie from a headers object or cookie string. */
20
+ declare function getCookie(headers: Record<string, string> | string, name: string): string | undefined;
21
+ /** Parse all cookies from a headers object or cookie string. */
22
+ declare function parseCookies(headers: Record<string, string> | string): Record<string, string>;
23
+ interface CookieOptions {
24
+ /** Cookie expiry in seconds from now. */
25
+ maxAge?: number;
26
+ /** Absolute expiry date. */
27
+ expires?: Date;
28
+ /** Cookie path. Default: "/" */
29
+ path?: string;
30
+ /** Cookie domain. */
31
+ domain?: string;
32
+ /** HTTPS only. Default: true in production. */
33
+ secure?: boolean;
34
+ /** Prevent JavaScript access. Default: true */
35
+ httpOnly?: boolean;
36
+ /** SameSite policy. Default: "lax" */
37
+ sameSite?: 'strict' | 'lax' | 'none';
38
+ }
39
+ /** Serialize a Set-Cookie header value. */
40
+ declare function setCookie(name: string, value: string, options?: CookieOptions): string;
41
+ /** Create a Set-Cookie header that deletes a cookie. */
42
+ declare function deleteCookie(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): string;
43
+ //#endregion
44
+ export { CookieOptions, deleteCookie, getCookie, parseCookies, setCookie };