silgi 0.0.14 → 0.1.0-beta.2
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 +102 -1
- package/dist/_virtual/_rolldown/runtime.mjs +5 -0
- package/dist/adapters/astro.d.mts +17 -0
- package/dist/adapters/astro.mjs +24 -0
- package/dist/adapters/aws-lambda.d.mts +31 -0
- package/dist/adapters/aws-lambda.mjs +85 -0
- package/dist/adapters/elysia.d.mts +17 -0
- package/dist/adapters/elysia.mjs +76 -0
- package/dist/adapters/express.d.mts +16 -0
- package/dist/adapters/express.mjs +78 -0
- package/dist/adapters/fastify.d.mts +15 -0
- package/dist/adapters/fastify.mjs +78 -0
- package/dist/adapters/message-port.d.mts +37 -0
- package/dist/adapters/message-port.mjs +129 -0
- package/dist/adapters/nestjs.d.mts +25 -0
- package/dist/adapters/nestjs.mjs +91 -0
- package/dist/adapters/nextjs.d.mts +21 -0
- package/dist/adapters/nextjs.mjs +30 -0
- package/dist/adapters/peer.d.mts +27 -0
- package/dist/adapters/peer.mjs +36 -0
- package/dist/adapters/remix.d.mts +17 -0
- package/dist/adapters/remix.mjs +24 -0
- package/dist/adapters/solidstart.d.mts +14 -0
- package/dist/adapters/solidstart.mjs +30 -0
- package/dist/adapters/sveltekit.d.mts +18 -0
- package/dist/adapters/sveltekit.mjs +33 -0
- package/dist/analyze.mjs +26 -0
- package/dist/broker/index.d.mts +62 -0
- package/dist/broker/index.mjs +153 -0
- package/dist/broker/nats.d.mts +33 -0
- package/dist/broker/nats.mjs +31 -0
- package/dist/broker/redis.d.mts +51 -0
- package/dist/broker/redis.mjs +92 -0
- package/dist/builder.d.mts +36 -0
- package/dist/builder.mjs +51 -0
- package/dist/callable.d.mts +17 -0
- package/dist/callable.mjs +42 -0
- package/dist/client/adapters/fetch/index.d.mts +17 -0
- package/dist/client/adapters/fetch/index.mjs +61 -0
- package/dist/client/adapters/ofetch/index.d.mts +41 -0
- package/dist/client/adapters/ofetch/index.mjs +92 -0
- package/dist/client/client.d.mts +29 -0
- package/dist/client/client.mjs +54 -0
- package/dist/client/dynamic-link.d.mts +15 -0
- package/dist/client/dynamic-link.mjs +16 -0
- package/dist/client/index.d.mts +7 -0
- package/dist/client/index.mjs +6 -0
- package/dist/client/interceptor.d.mts +31 -0
- package/dist/client/interceptor.mjs +34 -0
- package/dist/client/merge.d.mts +28 -0
- package/dist/client/merge.mjs +30 -0
- package/dist/client/openapi.d.mts +29 -0
- package/dist/client/openapi.mjs +89 -0
- package/dist/client/plugins/batch.d.mts +20 -0
- package/dist/client/plugins/batch.mjs +64 -0
- package/dist/client/plugins/csrf.d.mts +13 -0
- package/dist/client/plugins/csrf.mjs +20 -0
- package/dist/client/plugins/dedupe.d.mts +10 -0
- package/dist/client/plugins/dedupe.mjs +28 -0
- package/dist/client/plugins/index.d.mts +5 -0
- package/dist/client/plugins/index.mjs +5 -0
- package/dist/client/plugins/retry.d.mts +11 -0
- package/dist/client/plugins/retry.mjs +21 -0
- package/dist/client/server.d.mts +16 -0
- package/dist/client/server.mjs +60 -0
- package/dist/client/types.d.mts +29 -0
- package/dist/codec/devalue.d.mts +21 -0
- package/dist/codec/devalue.mjs +32 -0
- package/dist/codec/msgpack.d.mts +21 -0
- package/dist/codec/msgpack.mjs +59 -0
- package/dist/compile.d.mts +54 -0
- package/dist/compile.mjs +305 -0
- package/dist/contract.d.mts +36 -0
- package/dist/contract.mjs +40 -0
- package/dist/core/error.d.mts +104 -0
- package/dist/core/error.mjs +139 -0
- package/dist/core/handler.mjs +546 -0
- package/dist/core/iterator.d.mts +17 -0
- package/dist/core/iterator.mjs +79 -0
- package/dist/core/router-utils.mjs +16 -0
- package/dist/core/schema.d.mts +19 -0
- package/dist/core/schema.mjs +26 -0
- package/dist/core/serve.mjs +38 -0
- package/dist/core/sse.d.mts +16 -0
- package/dist/core/sse.mjs +95 -0
- package/dist/core/storage.d.mts +21 -0
- package/dist/core/storage.mjs +63 -0
- package/dist/core/utils.mjs +21 -0
- package/dist/fast-stringify.mjs +125 -0
- package/dist/index.d.mts +15 -37
- package/dist/index.mjs +13 -7
- package/dist/integrations/ai/index.d.mts +25 -0
- package/dist/integrations/ai/index.mjs +116 -0
- package/dist/integrations/react/index.d.mts +83 -0
- package/dist/integrations/react/index.mjs +197 -0
- package/dist/integrations/tanstack-query/index.d.mts +120 -0
- package/dist/integrations/tanstack-query/index.mjs +100 -0
- package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
- package/dist/integrations/tanstack-query/ssr.mjs +89 -0
- package/dist/integrations/zod/converter.d.mts +75 -0
- package/dist/integrations/zod/converter.mjs +345 -0
- package/dist/integrations/zod/index.d.mts +2 -0
- package/dist/integrations/zod/index.mjs +2 -0
- package/dist/lazy.d.mts +24 -0
- package/dist/lazy.mjs +27 -0
- package/dist/lifecycle.d.mts +36 -0
- package/dist/lifecycle.mjs +46 -0
- package/dist/map-input.d.mts +17 -0
- package/dist/map-input.mjs +24 -0
- package/dist/plugins/analytics.d.mts +168 -0
- package/dist/plugins/analytics.mjs +459 -0
- package/dist/plugins/batch-server.d.mts +20 -0
- package/dist/plugins/batch-server.mjs +86 -0
- package/dist/plugins/body-limit.d.mts +16 -0
- package/dist/plugins/body-limit.mjs +44 -0
- package/dist/plugins/cache.d.mts +170 -0
- package/dist/plugins/cache.mjs +200 -0
- package/dist/plugins/coerce.d.mts +21 -0
- package/dist/plugins/coerce.mjs +46 -0
- package/dist/plugins/compression.d.mts +19 -0
- package/dist/plugins/compression.mjs +23 -0
- package/dist/plugins/cookies.d.mts +44 -0
- package/dist/plugins/cookies.mjs +67 -0
- package/dist/plugins/cors.d.mts +39 -0
- package/dist/plugins/cors.mjs +56 -0
- package/dist/plugins/custom-serializer.d.mts +57 -0
- package/dist/plugins/custom-serializer.mjs +40 -0
- package/dist/plugins/file-upload.d.mts +38 -0
- package/dist/plugins/file-upload.mjs +100 -0
- package/dist/plugins/index.d.mts +16 -0
- package/dist/plugins/index.mjs +16 -0
- package/dist/plugins/otel.d.mts +35 -0
- package/dist/plugins/otel.mjs +40 -0
- package/dist/plugins/pino.d.mts +60 -0
- package/dist/plugins/pino.mjs +42 -0
- package/dist/plugins/pubsub.d.mts +50 -0
- package/dist/plugins/pubsub.mjs +53 -0
- package/dist/plugins/ratelimit.d.mts +51 -0
- package/dist/plugins/ratelimit.mjs +81 -0
- package/dist/plugins/signing.d.mts +41 -0
- package/dist/plugins/signing.mjs +115 -0
- package/dist/plugins/strict-get.d.mts +10 -0
- package/dist/plugins/strict-get.mjs +33 -0
- package/dist/route/add.mjs +240 -0
- package/dist/route/compiler.mjs +373 -0
- package/dist/route/context.mjs +12 -0
- package/dist/route/types.d.mts +11 -0
- package/dist/route/utils.mjs +17 -0
- package/dist/scalar.d.mts +53 -0
- package/dist/scalar.mjs +330 -0
- package/dist/silgi.d.mts +139 -0
- package/dist/silgi.mjs +113 -0
- package/dist/trpc-interop.d.mts +22 -0
- package/dist/trpc-interop.mjs +68 -0
- package/dist/types.d.mts +82 -0
- package/dist/ws.d.mts +42 -0
- package/dist/ws.mjs +137 -0
- package/lib/dashboard/index.html +123 -0
- package/lib/ocache.d.mts +1 -0
- package/lib/ocache.mjs +1 -0
- package/lib/ofetch.d.mts +1 -0
- package/lib/ofetch.mjs +1 -0
- package/lib/srvx.d.mts +1 -0
- package/lib/srvx.mjs +1 -0
- package/lib/unstorage.d.mts +1 -0
- package/lib/unstorage.mjs +1 -0
- package/package.json +291 -65
- package/bin/silgi.mjs +0 -3
- package/dist/chunks/generate.mjs +0 -933
- package/dist/chunks/init.mjs +0 -21
- package/dist/cli/config.d.mts +0 -19
- package/dist/cli/config.d.ts +0 -19
- package/dist/cli/config.mjs +0 -5
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +0 -119
- package/dist/index.d.ts +0 -37
- package/dist/plugins/openapi.d.mts +0 -138
- package/dist/plugins/openapi.d.ts +0 -138
- package/dist/plugins/openapi.mjs +0 -204
- package/dist/plugins/scalar.d.mts +0 -14
- package/dist/plugins/scalar.d.ts +0 -14
- package/dist/plugins/scalar.mjs +0 -66
- package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
- package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
- 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 };
|