stitchkit 0.37.0 → 0.39.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 +1 -1
- package/dist/browser/client-url.d.ts +13 -0
- package/dist/browser/client-url.d.ts.map +1 -0
- package/dist/browser/client.d.ts +41 -5
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +10 -7
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +102 -4
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
- package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
- package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
- package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
- package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
- package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -111
- package/dist/internal/route-pattern.d.ts +7 -0
- package/dist/internal/route-pattern.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/observability/index.js +1 -1
- package/dist/react/entity-cache.d.ts +56 -42
- package/dist/react/entity-cache.d.ts.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +98 -46
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.js +22 -7
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/response-metadata.d.ts +9 -0
- package/dist/server/response-metadata.d.ts.map +1 -0
- package/dist/server/router.d.ts +2 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/types.d.ts +14 -8
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +3 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/invoker.d.ts +33 -0
- package/dist/tools/invoker.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +6 -33
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +58 -0
- package/dist/tools/runtime-tool.d.ts.map +1 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +295 -182
- package/llms-full.txt +562 -79
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,20 +14,16 @@ import {
|
|
|
14
14
|
isStitchErrorCode,
|
|
15
15
|
notFound,
|
|
16
16
|
paginatedSchema,
|
|
17
|
+
parseTrailingWildcard,
|
|
17
18
|
rateLimited,
|
|
18
19
|
unauthorized
|
|
19
|
-
} from "./index-
|
|
20
|
+
} from "./index-xax049k6.js";
|
|
20
21
|
import {
|
|
21
22
|
isRecord,
|
|
22
23
|
mapObject,
|
|
23
24
|
typedEntries
|
|
24
25
|
} from "./index-809wc1tt.js";
|
|
25
26
|
|
|
26
|
-
// src/internal/http-input.ts
|
|
27
|
-
function inputIsQuery(method) {
|
|
28
|
-
return method === "GET" || method === "DELETE";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
// src/browser/client-multipart.ts
|
|
32
28
|
function isFileDescriptor(value) {
|
|
33
29
|
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
@@ -47,6 +43,170 @@ function appendFormFields(formData, values, skipKeys) {
|
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
46
|
+
// src/internal/http-input.ts
|
|
47
|
+
function inputIsQuery(method) {
|
|
48
|
+
return method === "GET" || method === "DELETE";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/browser/client-url.ts
|
|
52
|
+
function isParamArray(value) {
|
|
53
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
54
|
+
}
|
|
55
|
+
function collectQueryParams(args, endpoint) {
|
|
56
|
+
const params = {};
|
|
57
|
+
let hasParams = false;
|
|
58
|
+
for (const [key, value] of Object.entries(args)) {
|
|
59
|
+
if (value === undefined || value === null)
|
|
60
|
+
continue;
|
|
61
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
62
|
+
params[key] = value;
|
|
63
|
+
hasParams = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
67
|
+
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
68
|
+
}
|
|
69
|
+
return hasParams ? params : undefined;
|
|
70
|
+
}
|
|
71
|
+
function hasStringKeys(args, keys) {
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
if (typeof args[key] !== "string")
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
function resolvePathPrefix(config, args) {
|
|
79
|
+
if (!config?.pathPrefix)
|
|
80
|
+
return "";
|
|
81
|
+
if (typeof config.pathPrefix === "string")
|
|
82
|
+
return config.pathPrefix;
|
|
83
|
+
const keys = config.stripPrefixKeys ?? [];
|
|
84
|
+
if (!hasStringKeys(args, keys)) {
|
|
85
|
+
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
86
|
+
throw new Error(`Missing path prefix key: ${missing}`);
|
|
87
|
+
}
|
|
88
|
+
return config.pathPrefix(args);
|
|
89
|
+
}
|
|
90
|
+
function escapeRegex(value) {
|
|
91
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
92
|
+
}
|
|
93
|
+
function decodePathSegment(value) {
|
|
94
|
+
try {
|
|
95
|
+
return decodeURIComponent(value);
|
|
96
|
+
} catch {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function createClientRouteMatcher(endpoint, contractPrefix, config) {
|
|
101
|
+
if (typeof config?.pathPrefix === "function" && (!config.stripPrefixKeys || config.stripPrefixKeys.length === 0)) {
|
|
102
|
+
throw new Error("Dynamic pathPrefix matchers require stripPrefixKeys");
|
|
103
|
+
}
|
|
104
|
+
const markerByKey = {};
|
|
105
|
+
for (const [index, key] of (config?.stripPrefixKeys ?? []).entries()) {
|
|
106
|
+
markerByKey[key] = `__stitch_scope_${index}__`;
|
|
107
|
+
}
|
|
108
|
+
const pathPrefix = resolvePathPrefix(config, markerByKey);
|
|
109
|
+
const route = [pathPrefix, contractPrefix, endpoint.path === "/" ? "" : endpoint.path].filter(Boolean).join("/");
|
|
110
|
+
const patternSegments = route.split("/").filter(Boolean);
|
|
111
|
+
const wildcard = patternSegments.at(-1)?.startsWith("*") === true;
|
|
112
|
+
const fixedCount = wildcard ? patternSegments.length - 1 : patternSegments.length;
|
|
113
|
+
const markers = Object.values(markerByKey);
|
|
114
|
+
return (pathname) => {
|
|
115
|
+
const actualSegments = pathname.split("/").filter(Boolean).map(decodePathSegment);
|
|
116
|
+
if (wildcard ? actualSegments.length < fixedCount : actualSegments.length !== fixedCount) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
for (let index = 0;index < fixedCount; index += 1) {
|
|
120
|
+
const pattern = patternSegments[index];
|
|
121
|
+
const actual = actualSegments[index];
|
|
122
|
+
if (!pattern || actual === undefined)
|
|
123
|
+
return false;
|
|
124
|
+
if (pattern.startsWith(":"))
|
|
125
|
+
continue;
|
|
126
|
+
let source = escapeRegex(pattern);
|
|
127
|
+
for (const marker of markers) {
|
|
128
|
+
source = source.replaceAll(escapeRegex(marker), "[^/]+");
|
|
129
|
+
}
|
|
130
|
+
if (!new RegExp(`^${source}$`).test(actual))
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function extractParamNames(path) {
|
|
137
|
+
const matches = path.match(/:(\w+)/g);
|
|
138
|
+
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
139
|
+
const wildcard = parseTrailingWildcard(path);
|
|
140
|
+
if (wildcard)
|
|
141
|
+
names.push(wildcard.name);
|
|
142
|
+
return names;
|
|
143
|
+
}
|
|
144
|
+
function fillPathParams(path, args) {
|
|
145
|
+
const wildcard = parseTrailingWildcard(path);
|
|
146
|
+
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
147
|
+
const value = args[key];
|
|
148
|
+
if (value === undefined || value === null) {
|
|
149
|
+
throw new Error(`Missing path param: ${key}`);
|
|
150
|
+
}
|
|
151
|
+
return encodeURIComponent(String(value));
|
|
152
|
+
});
|
|
153
|
+
if (!wildcard)
|
|
154
|
+
return filled;
|
|
155
|
+
const wildcardValue = args[wildcard.name];
|
|
156
|
+
if (wildcardValue === undefined || wildcardValue === null) {
|
|
157
|
+
throw new Error(`Missing path param: ${wildcard.name}`);
|
|
158
|
+
}
|
|
159
|
+
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
160
|
+
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
161
|
+
return filled;
|
|
162
|
+
}
|
|
163
|
+
function stripConsumedArgs(args, path, scopeKeys) {
|
|
164
|
+
const consumed = new Set(scopeKeys);
|
|
165
|
+
for (const name of extractParamNames(path))
|
|
166
|
+
consumed.add(name);
|
|
167
|
+
const remaining = {};
|
|
168
|
+
for (const [key, value] of Object.entries(args)) {
|
|
169
|
+
if (!consumed.has(key) && value !== undefined)
|
|
170
|
+
remaining[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return remaining;
|
|
173
|
+
}
|
|
174
|
+
function appendQuery(relativeUrl, params) {
|
|
175
|
+
if (!params)
|
|
176
|
+
return relativeUrl;
|
|
177
|
+
const search = new URLSearchParams;
|
|
178
|
+
for (const [key, value] of Object.entries(params)) {
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
for (const item of value)
|
|
181
|
+
search.append(key, String(item));
|
|
182
|
+
} else {
|
|
183
|
+
search.set(key, String(value));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
187
|
+
}
|
|
188
|
+
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
189
|
+
let pathPrefix = resolvePathPrefix(config, args);
|
|
190
|
+
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
191
|
+
pathPrefix += "/";
|
|
192
|
+
if (pathPrefix.startsWith("/"))
|
|
193
|
+
pathPrefix = pathPrefix.slice(1);
|
|
194
|
+
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
195
|
+
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
196
|
+
if (relativeUrl.endsWith("/"))
|
|
197
|
+
relativeUrl = relativeUrl.slice(0, -1);
|
|
198
|
+
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
199
|
+
if (inputIsQuery(endpoint.method)) {
|
|
200
|
+
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
201
|
+
}
|
|
202
|
+
return { relativeUrl, remainingArgs };
|
|
203
|
+
}
|
|
204
|
+
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
205
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
206
|
+
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
207
|
+
return `${base}${path}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
50
210
|
// src/browser/http.ts
|
|
51
211
|
import ky, { isHTTPError } from "ky";
|
|
52
212
|
|
|
@@ -121,7 +281,7 @@ function createHttpClient(config) {
|
|
|
121
281
|
let ssrCookies = null;
|
|
122
282
|
let isLoggedOut = false;
|
|
123
283
|
const listeners = new Set;
|
|
124
|
-
const
|
|
284
|
+
const suppressUnauthorizedFor = config.suppressUnauthorizedFor ?? [];
|
|
125
285
|
const parseError = config.parseError ?? parseApiErrorBody;
|
|
126
286
|
function emit(event) {
|
|
127
287
|
for (const fn of listeners) {
|
|
@@ -160,7 +320,7 @@ function createHttpClient(config) {
|
|
|
160
320
|
async ({ request: request2, response }) => {
|
|
161
321
|
if (response.status === 401) {
|
|
162
322
|
const url = new URL(request2.url).pathname;
|
|
163
|
-
if (!isLoggedOut && !
|
|
323
|
+
if (!isLoggedOut && !suppressUnauthorizedFor.some((matches) => matches(url))) {
|
|
164
324
|
isLoggedOut = true;
|
|
165
325
|
emit({ type: "unauthorized" });
|
|
166
326
|
}
|
|
@@ -228,7 +388,9 @@ function createHttpClient(config) {
|
|
|
228
388
|
}
|
|
229
389
|
}
|
|
230
390
|
return {
|
|
391
|
+
baseUrl: config.baseUrl,
|
|
231
392
|
get: (url, options) => request("get", url, undefined, options),
|
|
393
|
+
head: (url, options) => request("head", url, undefined, options),
|
|
232
394
|
post: (url, data, options) => request("post", url, data, options),
|
|
233
395
|
put: (url, data, options) => request("put", url, data, options),
|
|
234
396
|
patch: (url, data, options) => request("patch", url, data, options),
|
|
@@ -267,24 +429,24 @@ function withOutput(endpoint, result) {
|
|
|
267
429
|
return result;
|
|
268
430
|
return result.then((value) => value === undefined ? undefined : schema.parse(value));
|
|
269
431
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let hasParams = false;
|
|
276
|
-
for (const [key, value] of Object.entries(args)) {
|
|
277
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
432
|
+
function contractEndpointMatchers(contract, endpointNames, contractConfig) {
|
|
433
|
+
const selected = endpointNames ? new Set(endpointNames) : null;
|
|
434
|
+
const matchers = [];
|
|
435
|
+
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
436
|
+
if (selected && !selected.has(key))
|
|
278
437
|
continue;
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
438
|
+
if (endpoint.expose && !endpoint.expose.includes("HTTP")) {
|
|
439
|
+
if (selected) {
|
|
440
|
+
throw new Error(`Cannot create an HTTP route matcher for non-HTTP endpoint: ${String(key)}`);
|
|
441
|
+
}
|
|
282
442
|
continue;
|
|
283
443
|
}
|
|
284
|
-
|
|
285
|
-
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
444
|
+
matchers.push(createClientRouteMatcher(endpoint, contract.meta.prefix, contractConfig));
|
|
286
445
|
}
|
|
287
|
-
|
|
446
|
+
if (selected && matchers.length !== selected.size) {
|
|
447
|
+
throw new Error("Cannot create an HTTP route matcher for an unknown endpoint");
|
|
448
|
+
}
|
|
449
|
+
return matchers;
|
|
288
450
|
}
|
|
289
451
|
function createClient(contract, configOrClient, contractConfig) {
|
|
290
452
|
const client = {};
|
|
@@ -296,8 +458,34 @@ function createClient(contract, configOrClient, contractConfig) {
|
|
|
296
458
|
}
|
|
297
459
|
return client;
|
|
298
460
|
}
|
|
299
|
-
function createClients(contracts,
|
|
300
|
-
return mapObject(contracts, (_key, contract) => createClient(contract,
|
|
461
|
+
function createClients(contracts, configOrClient, contractConfig) {
|
|
462
|
+
return mapObject(contracts, (_key, contract) => createClient(contract, configOrClient, contractConfig));
|
|
463
|
+
}
|
|
464
|
+
function createScopedClients(contracts, configOrClient, scopeConfigs) {
|
|
465
|
+
const registry = {};
|
|
466
|
+
for (const [namespace, value] of Object.entries(contracts)) {
|
|
467
|
+
const list = Array.isArray(value) ? value : [value];
|
|
468
|
+
const client = {};
|
|
469
|
+
for (const contract of list) {
|
|
470
|
+
const scope = contract.meta.scope;
|
|
471
|
+
if (!scope) {
|
|
472
|
+
throw new Error(`Contract in client namespace "${namespace}" has no scope`);
|
|
473
|
+
}
|
|
474
|
+
const scopeConfig = Object.entries(scopeConfigs).find(([configuredScope]) => configuredScope === scope)?.[1];
|
|
475
|
+
if (!scopeConfig) {
|
|
476
|
+
throw new Error(`Missing client config for scope: ${scope}`);
|
|
477
|
+
}
|
|
478
|
+
const scoped = createClient(contract, configOrClient, scopeConfig);
|
|
479
|
+
for (const [methodName, method] of Object.entries(scoped)) {
|
|
480
|
+
if (Object.hasOwn(client, methodName)) {
|
|
481
|
+
throw new Error(`Client namespace "${namespace}" has duplicate method: ${methodName}`);
|
|
482
|
+
}
|
|
483
|
+
client[methodName] = method;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
registry[namespace] = client;
|
|
487
|
+
}
|
|
488
|
+
return registry;
|
|
301
489
|
}
|
|
302
490
|
function isHttpAdapter(value) {
|
|
303
491
|
return typeof value === "object" && "get" in value && typeof value.get === "function";
|
|
@@ -307,90 +495,41 @@ function setClientMethod(target, key, method) {
|
|
|
307
495
|
}
|
|
308
496
|
function createHttpMethod(endpoint, prefix, client, config) {
|
|
309
497
|
const httpMethod = endpoint.method.toLowerCase();
|
|
310
|
-
const isGet = httpMethod === "get";
|
|
311
|
-
const paramNames = extractParamNames(endpoint.path);
|
|
312
|
-
const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
|
|
313
498
|
return (...args) => {
|
|
314
499
|
const firstArg = args[0] ?? {};
|
|
315
|
-
|
|
316
|
-
if (config?.pathPrefix) {
|
|
317
|
-
pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
|
|
318
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
319
|
-
pathPrefixStr += "/";
|
|
320
|
-
}
|
|
321
|
-
let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
|
|
322
|
-
for (const name of paramNames) {
|
|
323
|
-
const value = firstArg[name];
|
|
324
|
-
if (value === undefined || value === null) {
|
|
325
|
-
throw new Error(`Missing path param: ${name}`);
|
|
326
|
-
}
|
|
327
|
-
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
328
|
-
}
|
|
329
|
-
if (url.endsWith("/"))
|
|
330
|
-
url = url.slice(0, -1);
|
|
500
|
+
const plan = planClientRequest(endpoint, prefix, firstArg, config);
|
|
331
501
|
if (endpoint.multipart) {
|
|
332
502
|
const file = firstArg[endpoint.multipart];
|
|
333
503
|
if (!isMultipartFile(file)) {
|
|
334
504
|
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
335
505
|
}
|
|
336
|
-
if (httpMethod === "get" || httpMethod === "delete") {
|
|
506
|
+
if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
|
|
337
507
|
throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
|
|
338
508
|
}
|
|
339
509
|
const formData = new FormData;
|
|
340
510
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
341
|
-
appendFormFields(formData,
|
|
342
|
-
return withOutput(endpoint, client[httpMethod](
|
|
511
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
512
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
|
|
343
513
|
}
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
514
|
+
if (httpMethod === "get" || httpMethod === "head") {
|
|
515
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
347
516
|
}
|
|
348
517
|
if (httpMethod === "delete") {
|
|
349
|
-
|
|
350
|
-
return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
518
|
+
return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
351
519
|
}
|
|
352
|
-
|
|
353
|
-
for (const [key, value] of Object.entries(firstArg)) {
|
|
354
|
-
if (!prefixKeys.has(key) && value !== undefined) {
|
|
355
|
-
payload[key] = value;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return withOutput(endpoint, client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint)));
|
|
520
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
|
|
359
521
|
};
|
|
360
522
|
}
|
|
361
523
|
function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
362
|
-
const prefixKeys = new Set(contractConfig?.stripPrefixKeys ?? []);
|
|
363
524
|
return async (args) => {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
pathPrefixStr = typeof contractConfig.pathPrefix === "function" ? contractConfig.pathPrefix(args ?? {}) : contractConfig.pathPrefix;
|
|
367
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
368
|
-
pathPrefixStr += "/";
|
|
369
|
-
}
|
|
370
|
-
let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args, pathPrefixStr);
|
|
525
|
+
const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
|
|
526
|
+
const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
|
|
371
527
|
const headers = {
|
|
372
528
|
Accept: "application/json",
|
|
373
529
|
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
374
530
|
};
|
|
375
531
|
const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
|
|
376
|
-
const
|
|
377
|
-
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
378
|
-
if (isQuery && args) {
|
|
379
|
-
const remaining = stripParams(args, endpoint.path, prefixKeys);
|
|
380
|
-
const params = collectQueryParams(remaining, new Set, endpoint);
|
|
381
|
-
if (params) {
|
|
382
|
-
const searchParams = new URLSearchParams;
|
|
383
|
-
for (const [k, v] of Object.entries(params)) {
|
|
384
|
-
if (Array.isArray(v)) {
|
|
385
|
-
for (const item of v)
|
|
386
|
-
searchParams.append(k, String(item));
|
|
387
|
-
} else {
|
|
388
|
-
searchParams.set(k, String(v));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
url += `?${searchParams}`;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
532
|
+
const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
|
|
394
533
|
if (hasBody)
|
|
395
534
|
headers["Content-Type"] = "application/json";
|
|
396
535
|
if (endpoint.multipart && args) {
|
|
@@ -400,7 +539,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
400
539
|
}
|
|
401
540
|
const formData = new FormData;
|
|
402
541
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
403
|
-
appendFormFields(formData,
|
|
542
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
404
543
|
const res2 = await fetch(url, {
|
|
405
544
|
method: endpoint.method,
|
|
406
545
|
headers,
|
|
@@ -424,7 +563,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
424
563
|
credentials: config.credentials,
|
|
425
564
|
signal,
|
|
426
565
|
...hasBody && {
|
|
427
|
-
body: JSON.stringify(
|
|
566
|
+
body: JSON.stringify(plan.remainingArgs)
|
|
428
567
|
}
|
|
429
568
|
});
|
|
430
569
|
if (!res.ok) {
|
|
@@ -447,35 +586,24 @@ async function throwForErrorResponse(res, config, fallbackBody) {
|
|
|
447
586
|
}
|
|
448
587
|
throw new ApiError("HTTP_ERROR", res.status, { body });
|
|
449
588
|
}
|
|
450
|
-
function
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
throw new Error(`Missing path param: ${key}`);
|
|
589
|
+
function createUrlBuilder(contract, source, contractConfig) {
|
|
590
|
+
const builder = {};
|
|
591
|
+
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
592
|
+
if (endpoint.expose && !endpoint.expose.includes("HTTP"))
|
|
593
|
+
continue;
|
|
594
|
+
setClientMethod(builder, key, (args) => {
|
|
595
|
+
const plan = planClientRequest(endpoint, contract.meta.prefix, args ?? {}, contractConfig);
|
|
596
|
+
if (endpoint.method !== "GET" && endpoint.method !== "DELETE" && Object.keys(plan.remainingArgs).length > 0) {
|
|
597
|
+
const fields = Object.keys(plan.remainingArgs).join(", ");
|
|
598
|
+
throw new Error(`URL builder for ${endpoint.method} ${endpoint.path} received non-URL fields: ${fields}`);
|
|
461
599
|
}
|
|
462
|
-
return
|
|
600
|
+
return joinClientBaseUrl(source.baseUrl, plan.relativeUrl);
|
|
463
601
|
});
|
|
464
602
|
}
|
|
465
|
-
return
|
|
603
|
+
return builder;
|
|
466
604
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
for (const match of path.matchAll(/:(\w+)/g)) {
|
|
470
|
-
if (match[1])
|
|
471
|
-
skip.add(match[1]);
|
|
472
|
-
}
|
|
473
|
-
const result = {};
|
|
474
|
-
for (const [k, v] of Object.entries(args)) {
|
|
475
|
-
if (!skip.has(k))
|
|
476
|
-
result[k] = v;
|
|
477
|
-
}
|
|
478
|
-
return result;
|
|
605
|
+
function createUrlBuilders(contracts, source, contractConfig) {
|
|
606
|
+
return mapObject(contracts, (_key, contract) => createUrlBuilder(contract, source, contractConfig));
|
|
479
607
|
}
|
|
480
608
|
// src/retained.ts
|
|
481
609
|
function createRetainedTopics() {
|
|
@@ -683,13 +811,17 @@ export {
|
|
|
683
811
|
defineErrors,
|
|
684
812
|
defineContract,
|
|
685
813
|
decodeCursor,
|
|
814
|
+
createUrlBuilders,
|
|
815
|
+
createUrlBuilder,
|
|
686
816
|
createTraceContext,
|
|
687
817
|
createSocketIOClient,
|
|
818
|
+
createScopedClients,
|
|
688
819
|
createRetainedTopics,
|
|
689
820
|
createHttpClient,
|
|
690
821
|
createContractFactory,
|
|
691
822
|
createClients,
|
|
692
823
|
createClient,
|
|
824
|
+
contractEndpointMatchers,
|
|
693
825
|
conflict,
|
|
694
826
|
childSpan,
|
|
695
827
|
badRequest,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface TrailingWildcard {
|
|
2
|
+
name: string;
|
|
3
|
+
segmentIndex: number;
|
|
4
|
+
}
|
|
5
|
+
/** Parse and validate the route's single named terminal wildcard, if present. */
|
|
6
|
+
export declare function parseTrailingWildcard(path: string): TrailingWildcard | null;
|
|
7
|
+
//# sourceMappingURL=route-pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-pattern.d.ts","sourceRoot":"","sources":["../../src/internal/route-pattern.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA2C3E"}
|
package/dist/node.js
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
createImplement,
|
|
4
4
|
createSocketIOServer,
|
|
5
5
|
implement
|
|
6
|
-
} from "./index-
|
|
7
|
-
import"./index-
|
|
6
|
+
} from "./index-7rbzbnnf.js";
|
|
7
|
+
import"./index-6jypn22c.js";
|
|
8
8
|
import"./index-x3fcszf8.js";
|
|
9
9
|
import {
|
|
10
10
|
AppError,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
notFound,
|
|
16
16
|
rateLimited,
|
|
17
17
|
unauthorized
|
|
18
|
-
} from "./index-
|
|
18
|
+
} from "./index-fyfk537k.js";
|
|
19
19
|
// src/server/node.ts
|
|
20
20
|
import { serve } from "srvx";
|
|
21
21
|
async function serveNode(config) {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
setRequestError,
|
|
17
17
|
setRequestUser,
|
|
18
18
|
wrapInRequestContext
|
|
19
|
-
} from "../index-
|
|
19
|
+
} from "../index-fyfk537k.js";
|
|
20
20
|
|
|
21
21
|
// src/observability/sanitize.ts
|
|
22
22
|
var DEFAULT_SENSITIVE_KEYS = /(password|passwd|pwd|secret|token|apikey|api[-_ ]?key|auth|authorization|bearer|session|cookie|init[-_ ]?data|credential|private[-_ ]?key)/i;
|
|
@@ -1,54 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Declarative CRUD cache handlers for `createCacheBridge
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* from a small config.
|
|
6
|
-
*
|
|
7
|
-
* It patches stitchkit's own `Paginated<T>` list envelope (plain or a TanStack
|
|
8
|
-
* `InfiniteData` of it) and the entity's detail query. It does **not** flatten
|
|
9
|
-
* pages or expose a `useAllX` surface — flattening stays in the component (a
|
|
10
|
-
* deliberate boundary): this only keeps the cache correct.
|
|
11
|
-
*
|
|
12
|
-
* ```ts
|
|
13
|
-
* const handlers = createEntityCacheHandlers<Widget>({
|
|
14
|
-
* getId: (w) => w.id,
|
|
15
|
-
* listKey: ['widgets'],
|
|
16
|
-
* detailKey: (id) => ['widgets', id],
|
|
17
|
-
* });
|
|
18
|
-
* createCacheBridge({ socket, queryClient, handlers: {
|
|
19
|
-
* widgetCreated: handlers.created,
|
|
20
|
-
* widgetUpdated: handlers.updated,
|
|
21
|
-
* widgetDeleted: handlers.deleted,
|
|
22
|
-
* }});
|
|
23
|
-
* ```
|
|
2
|
+
* Declarative CRUD cache handlers for `createCacheBridge`. One config projects
|
|
3
|
+
* a full event entity into the cached list item and patches plain, paginated or
|
|
4
|
+
* infinite TanStack Query data without changing its envelope metadata.
|
|
24
5
|
*/
|
|
25
6
|
import type { QueryKey } from '@tanstack/react-query';
|
|
26
7
|
import type { CacheBridgeHandler } from './cache-bridge';
|
|
27
|
-
/** The `deleted` event may carry the whole entity or
|
|
28
|
-
export type DeletedPayload<
|
|
8
|
+
/** The `deleted` event may carry the whole entity or only its id. */
|
|
9
|
+
export type DeletedPayload<TData> = TData | {
|
|
29
10
|
id: string;
|
|
30
11
|
};
|
|
12
|
+
/** Typed event passed to dynamic list/detail query-key selectors. */
|
|
13
|
+
export type EntityCacheEvent<TData> = {
|
|
14
|
+
type: 'created';
|
|
15
|
+
entity: TData;
|
|
16
|
+
id: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'updated';
|
|
19
|
+
entity: TData;
|
|
20
|
+
id: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'deleted';
|
|
23
|
+
payload: DeletedPayload<TData>;
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
/** A static cache key/prefix or an event-aware key factory. */
|
|
27
|
+
export type EntityCacheKey<TData> = QueryKey | ((event: EntityCacheEvent<TData>) => QueryKey);
|
|
28
|
+
export type EntityCacheListShape = 'array' | 'paginated' | 'infinite-array' | 'infinite-paginated';
|
|
29
|
+
/** List envelope, scoped key and explicit CRUD policies. */
|
|
30
|
+
export interface EntityCacheListConfig<TData, TListItem> {
|
|
31
|
+
/** Static query-key prefix or an event-aware scoped key factory. */
|
|
32
|
+
key: EntityCacheKey<TData>;
|
|
33
|
+
/** Edge/page used for a create or an inserted missing update. */
|
|
34
|
+
createAt: 'start' | 'end';
|
|
35
|
+
/** Explicit behavior when an update's id is absent from every cached page. */
|
|
36
|
+
updateMissing: 'skip' | 'insert';
|
|
37
|
+
/** Backend-equivalent ordering for each affected logical item array. */
|
|
38
|
+
compare?: (left: TListItem, right: TListItem) => number;
|
|
39
|
+
/** Cached data envelope; no runtime shape inference is performed. */
|
|
40
|
+
shape: EntityCacheListShape;
|
|
41
|
+
}
|
|
31
42
|
/** Config for `createEntityCacheHandlers`. */
|
|
32
|
-
export interface EntityCacheConfig<
|
|
33
|
-
/**
|
|
34
|
-
getId: (entity:
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
43
|
+
export interface EntityCacheConfig<TData, TListItem = TData> {
|
|
44
|
+
/** Canonical id from a full created/updated entity. */
|
|
45
|
+
getId: (entity: TData) => string;
|
|
46
|
+
/** Canonical id from the projected item stored in list caches. */
|
|
47
|
+
getListItemId: (item: TListItem) => string;
|
|
48
|
+
/** Project a full mutation entity into the list cache's item type. */
|
|
49
|
+
toListItem: (entity: TData) => TListItem;
|
|
50
|
+
/** Read a deleted id. Default: a string `.id`, otherwise `getId(payload)`. */
|
|
51
|
+
getDeletedId?: (payload: DeletedPayload<TData>) => string;
|
|
52
|
+
/** List envelope, key/prefix, insertion policy and optional ordering. */
|
|
53
|
+
list: EntityCacheListConfig<TData, TListItem>;
|
|
54
|
+
/** Static detail key or event-aware key factory. Omit to skip detail updates. */
|
|
55
|
+
detailKey?: EntityCacheKey<TData>;
|
|
41
56
|
}
|
|
42
|
-
/** The three handlers to wire onto a `createCacheBridge`
|
|
43
|
-
export interface EntityCacheHandlers<
|
|
44
|
-
created: CacheBridgeHandler<
|
|
45
|
-
updated: CacheBridgeHandler<
|
|
46
|
-
deleted: CacheBridgeHandler<DeletedPayload<
|
|
57
|
+
/** The three handlers to wire onto a `createCacheBridge` handlers map. */
|
|
58
|
+
export interface EntityCacheHandlers<TData> {
|
|
59
|
+
created: CacheBridgeHandler<TData>;
|
|
60
|
+
updated: CacheBridgeHandler<TData>;
|
|
61
|
+
deleted: CacheBridgeHandler<DeletedPayload<TData>>;
|
|
47
62
|
}
|
|
48
63
|
/**
|
|
49
|
-
* Build created
|
|
50
|
-
*
|
|
51
|
-
* the entity's detail key when available, else the list key.
|
|
64
|
+
* Build created/updated/deleted cache handlers over one declared list shape.
|
|
65
|
+
* Every event resolves its own scoped keys and applies the same fresh-echo gate.
|
|
52
66
|
*/
|
|
53
|
-
export declare function createEntityCacheHandlers<
|
|
67
|
+
export declare function createEntityCacheHandlers<TData, TListItem = TData>(config: EntityCacheConfig<TData, TListItem>): EntityCacheHandlers<TData>;
|
|
54
68
|
//# sourceMappingURL=entity-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-cache.d.ts","sourceRoot":"","sources":["../../src/react/entity-cache.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"entity-cache.d.ts","sourceRoot":"","sources":["../../src/react/entity-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGpE,OAAO,KAAK,EAAsB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,qEAAqE;AACrE,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,CAAC,KAAK,IAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,+DAA+D;AAC/D,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC;AAE9F,MAAM,MAAM,oBAAoB,GAC5B,OAAO,GACP,WAAW,GACX,gBAAgB,GAChB,oBAAoB,CAAC;AAEzB,4DAA4D;AAC5D,MAAM,WAAW,qBAAqB,CAAC,KAAK,EAAE,SAAS;IACrD,oEAAoE;IACpE,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAC1B,8EAA8E;IAC9E,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC;IACjC,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,MAAM,CAAC;IACxD,qEAAqE;IACrE,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AAED,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK;IACzD,uDAAuD;IACvD,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,MAAM,CAAC;IACjC,kEAAkE;IAClE,aAAa,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC;IAC3C,sEAAsE;IACtE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,SAAS,CAAC;IACzC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC;IAC1D,yEAAyE;IACzE,IAAI,EAAE,qBAAqB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC9C,iFAAiF;IACjF,SAAS,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;CACnC;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB,CAAC,KAAK;IACxC,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;CACpD;AAiJD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,EAChE,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,GAC1C,mBAAmB,CAAC,KAAK,CAAC,CAkD5B"}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { type CacheBridge, type CacheBridgeConfig, type CacheBridgeContext, type CacheBridgeHandler, type CacheBridgeHandlers, type CacheBridgeSocket, createCacheBridge, } from './react/cache-bridge';
|
|
2
2
|
export { type CursorQueryConfig, createCursorQuery } from './react/cursor-query';
|
|
3
|
-
export { createEntityCacheHandlers, type DeletedPayload, type EntityCacheConfig, type EntityCacheHandlers, } from './react/entity-cache';
|
|
3
|
+
export { createEntityCacheHandlers, type DeletedPayload, type EntityCacheConfig, type EntityCacheEvent, type EntityCacheHandlers, type EntityCacheKey, type EntityCacheListConfig, type EntityCacheListShape, } from './react/entity-cache';
|
|
4
4
|
//# sourceMappingURL=react.d.ts.map
|