@spikard/node 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -350
- package/dist/index.d.mts +400 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +1697 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1645 -0
- package/dist/index.mjs.map +1 -0
- package/index.d.ts +12 -1
- package/index.js +105 -105
- package/package.json +43 -38
- package/spikard-node.linux-x64-gnu.node +0 -0
- package/config.d.ts +0 -281
- package/graphql.d.ts +0 -287
package/dist/index.js
ADDED
|
@@ -0,0 +1,1697 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
GrpcError: () => GrpcError,
|
|
34
|
+
GrpcStatusCode: () => GrpcStatusCode,
|
|
35
|
+
Spikard: () => Spikard,
|
|
36
|
+
StreamingResponse: () => StreamingResponse,
|
|
37
|
+
TestClient: () => TestClient,
|
|
38
|
+
UploadFile: () => UploadFile,
|
|
39
|
+
background: () => background_exports,
|
|
40
|
+
createServiceHandler: () => createServiceHandler,
|
|
41
|
+
createUnaryHandler: () => createUnaryHandler,
|
|
42
|
+
del: () => del,
|
|
43
|
+
get: () => get,
|
|
44
|
+
patch: () => patch,
|
|
45
|
+
post: () => post,
|
|
46
|
+
put: () => put,
|
|
47
|
+
route: () => route,
|
|
48
|
+
runServer: () => runServer,
|
|
49
|
+
wrapBodyHandler: () => wrapBodyHandler,
|
|
50
|
+
wrapHandler: () => wrapHandler
|
|
51
|
+
});
|
|
52
|
+
module.exports = __toCommonJS(index_exports);
|
|
53
|
+
|
|
54
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
55
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
56
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
57
|
+
|
|
58
|
+
// src/server.ts
|
|
59
|
+
var import_node_module2 = require("module");
|
|
60
|
+
|
|
61
|
+
// src/upload.ts
|
|
62
|
+
var UploadFile = class {
|
|
63
|
+
/** Original filename from the client */
|
|
64
|
+
filename;
|
|
65
|
+
/** MIME type of the uploaded file */
|
|
66
|
+
contentType;
|
|
67
|
+
/** Size of the file in bytes */
|
|
68
|
+
size;
|
|
69
|
+
/** Additional headers associated with this file field */
|
|
70
|
+
headers;
|
|
71
|
+
/** Internal buffer storing file contents */
|
|
72
|
+
_content;
|
|
73
|
+
/** Current read position in the buffer */
|
|
74
|
+
_position = 0;
|
|
75
|
+
/**
|
|
76
|
+
* Create a new UploadFile instance
|
|
77
|
+
*
|
|
78
|
+
* @param filename - Original filename from the client
|
|
79
|
+
* @param content - File contents as Buffer
|
|
80
|
+
* @param contentType - MIME type (defaults to "application/octet-stream")
|
|
81
|
+
* @param size - File size in bytes (computed from content if not provided)
|
|
82
|
+
* @param headers - Additional headers from the multipart field
|
|
83
|
+
*/
|
|
84
|
+
constructor(filename, content, contentType = null, size = null, headers = null) {
|
|
85
|
+
this.filename = filename;
|
|
86
|
+
this.contentType = contentType ?? "application/octet-stream";
|
|
87
|
+
this.size = size ?? content.length;
|
|
88
|
+
this.headers = headers ?? {};
|
|
89
|
+
this._content = content;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read file contents synchronously
|
|
93
|
+
*
|
|
94
|
+
* @param size - Number of bytes to read (-1 for all remaining)
|
|
95
|
+
* @returns File contents as Buffer
|
|
96
|
+
*/
|
|
97
|
+
read(size = -1) {
|
|
98
|
+
if (size === -1) {
|
|
99
|
+
const result2 = this._content.subarray(this._position);
|
|
100
|
+
this._position = this._content.length;
|
|
101
|
+
return result2;
|
|
102
|
+
}
|
|
103
|
+
const end = Math.min(this._position + size, this._content.length);
|
|
104
|
+
const result = this._content.subarray(this._position, end);
|
|
105
|
+
this._position = end;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Read file contents asynchronously
|
|
110
|
+
*
|
|
111
|
+
* Since the file is already in memory from Rust parsing, this is a simple wrapper.
|
|
112
|
+
*
|
|
113
|
+
* @param size - Number of bytes to read (-1 for all remaining)
|
|
114
|
+
* @returns File contents as Buffer
|
|
115
|
+
*/
|
|
116
|
+
async readAsync(size = -1) {
|
|
117
|
+
return this.read(size);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read entire file as UTF-8 text
|
|
121
|
+
*
|
|
122
|
+
* @returns File contents as string
|
|
123
|
+
*/
|
|
124
|
+
text() {
|
|
125
|
+
return this._content.toString("utf-8");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read entire file as UTF-8 text asynchronously
|
|
129
|
+
*
|
|
130
|
+
* @returns File contents as string
|
|
131
|
+
*/
|
|
132
|
+
async textAsync() {
|
|
133
|
+
return this.text();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Seek to a position in the file
|
|
137
|
+
*
|
|
138
|
+
* @param offset - Position to seek to
|
|
139
|
+
* @param whence - How to interpret offset (0=absolute, 1=relative, 2=from end)
|
|
140
|
+
* @returns New absolute position
|
|
141
|
+
*/
|
|
142
|
+
seek(offset, whence = 0) {
|
|
143
|
+
switch (whence) {
|
|
144
|
+
case 0:
|
|
145
|
+
this._position = Math.max(0, Math.min(offset, this._content.length));
|
|
146
|
+
break;
|
|
147
|
+
case 1:
|
|
148
|
+
this._position = Math.max(0, Math.min(this._position + offset, this._content.length));
|
|
149
|
+
break;
|
|
150
|
+
case 2:
|
|
151
|
+
this._position = Math.max(0, Math.min(this._content.length + offset, this._content.length));
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`Invalid whence value: ${whence}`);
|
|
155
|
+
}
|
|
156
|
+
return this._position;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Seek to a position in the file asynchronously
|
|
160
|
+
*
|
|
161
|
+
* @param offset - Position to seek to
|
|
162
|
+
* @param whence - How to interpret offset (0=absolute, 1=relative, 2=from end)
|
|
163
|
+
* @returns New absolute position
|
|
164
|
+
*/
|
|
165
|
+
async seekAsync(offset, whence = 0) {
|
|
166
|
+
return this.seek(offset, whence);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get current position in the file
|
|
170
|
+
*
|
|
171
|
+
* @returns Current byte position
|
|
172
|
+
*/
|
|
173
|
+
tell() {
|
|
174
|
+
return this._position;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get the underlying Buffer
|
|
178
|
+
*
|
|
179
|
+
* @returns Complete file contents as Buffer
|
|
180
|
+
*/
|
|
181
|
+
getBuffer() {
|
|
182
|
+
return this._content;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Close the file (no-op for in-memory files, provided for API compatibility)
|
|
186
|
+
*/
|
|
187
|
+
close() {
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Close the file asynchronously (no-op, provided for API compatibility)
|
|
191
|
+
*/
|
|
192
|
+
async closeAsync() {
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* String representation of the upload file
|
|
196
|
+
*/
|
|
197
|
+
toString() {
|
|
198
|
+
return `UploadFile(filename=${JSON.stringify(this.filename)}, contentType=${JSON.stringify(this.contentType)}, size=${this.size})`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* JSON representation for debugging
|
|
202
|
+
*/
|
|
203
|
+
toJSON() {
|
|
204
|
+
return {
|
|
205
|
+
filename: this.filename,
|
|
206
|
+
contentType: this.contentType,
|
|
207
|
+
size: this.size,
|
|
208
|
+
headers: this.headers
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/converters.ts
|
|
214
|
+
function isFileMetadata(value) {
|
|
215
|
+
return typeof value === "object" && value !== null && "filename" in value && "content" in value;
|
|
216
|
+
}
|
|
217
|
+
function convertFileMetadataToUploadFile(fileData) {
|
|
218
|
+
const { filename, content, size, content_type, content_encoding } = fileData;
|
|
219
|
+
let buffer;
|
|
220
|
+
if (content_encoding === "base64") {
|
|
221
|
+
buffer = Buffer.from(content, "base64");
|
|
222
|
+
} else {
|
|
223
|
+
buffer = Buffer.from(content, "utf-8");
|
|
224
|
+
}
|
|
225
|
+
return new UploadFile(filename, buffer, content_type ?? null, size ?? null);
|
|
226
|
+
}
|
|
227
|
+
function processUploadFileFields(value) {
|
|
228
|
+
if (value === null || value === void 0) {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
if (typeof value !== "object") {
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(value)) {
|
|
235
|
+
return value.map((item) => {
|
|
236
|
+
if (isFileMetadata(item)) {
|
|
237
|
+
return convertFileMetadataToUploadFile(item);
|
|
238
|
+
}
|
|
239
|
+
return processUploadFileFields(item);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (isFileMetadata(value)) {
|
|
243
|
+
return convertFileMetadataToUploadFile(value);
|
|
244
|
+
}
|
|
245
|
+
const result = {};
|
|
246
|
+
for (const [key, val] of Object.entries(value)) {
|
|
247
|
+
result[key] = processUploadFileFields(val);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function convertMultipartTestPayload(payload) {
|
|
252
|
+
const result = {};
|
|
253
|
+
if (payload.fields) {
|
|
254
|
+
Object.assign(result, payload.fields);
|
|
255
|
+
}
|
|
256
|
+
if (payload.files && payload.files.length > 0) {
|
|
257
|
+
const filesByName = {};
|
|
258
|
+
for (const file of payload.files) {
|
|
259
|
+
const fileMetadata = {
|
|
260
|
+
filename: file.filename || file.name,
|
|
261
|
+
content: file.content,
|
|
262
|
+
content_type: file.contentType
|
|
263
|
+
};
|
|
264
|
+
const uploadFile = convertFileMetadataToUploadFile(fileMetadata);
|
|
265
|
+
if (!filesByName[file.name]) {
|
|
266
|
+
filesByName[file.name] = [];
|
|
267
|
+
}
|
|
268
|
+
const files = filesByName[file.name];
|
|
269
|
+
if (files) {
|
|
270
|
+
files.push(uploadFile);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const [name, files] of Object.entries(filesByName)) {
|
|
274
|
+
result[name] = files.length === 1 ? files[0] : files;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
function convertHandlerBody(body) {
|
|
280
|
+
if (typeof body === "object" && body !== null && "__spikard_multipart__" in body && typeof body.__spikard_multipart__ === "object") {
|
|
281
|
+
const multipart = body.__spikard_multipart__;
|
|
282
|
+
return convertMultipartTestPayload(multipart);
|
|
283
|
+
}
|
|
284
|
+
return processUploadFileFields(body);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/request.ts
|
|
288
|
+
var RequestImpl = class {
|
|
289
|
+
method;
|
|
290
|
+
path;
|
|
291
|
+
params;
|
|
292
|
+
pathParams;
|
|
293
|
+
query;
|
|
294
|
+
queryParams;
|
|
295
|
+
headers;
|
|
296
|
+
cookies;
|
|
297
|
+
body;
|
|
298
|
+
dependencies;
|
|
299
|
+
#jsonCache;
|
|
300
|
+
#formCache;
|
|
301
|
+
constructor(data) {
|
|
302
|
+
this.method = data.method;
|
|
303
|
+
this.path = data.path;
|
|
304
|
+
const pathParams = data.params ?? data.pathParams ?? {};
|
|
305
|
+
const queryParams = data.query ?? extractQueryParams(data.queryParams) ?? {};
|
|
306
|
+
this.params = pathParams;
|
|
307
|
+
this.pathParams = pathParams;
|
|
308
|
+
this.query = queryParams;
|
|
309
|
+
this.queryParams = queryParams;
|
|
310
|
+
this.headers = normalizeHeaders(data.headers ?? {});
|
|
311
|
+
this.cookies = data.cookies ?? {};
|
|
312
|
+
this.body = convertBodyToBuffer(data.body);
|
|
313
|
+
this.dependencies = data.dependencies;
|
|
314
|
+
}
|
|
315
|
+
json() {
|
|
316
|
+
if (this.#jsonCache !== void 0) {
|
|
317
|
+
return this.#jsonCache;
|
|
318
|
+
}
|
|
319
|
+
if (!this.body || this.body.length === 0) {
|
|
320
|
+
throw new Error("No body available to parse as JSON");
|
|
321
|
+
}
|
|
322
|
+
const raw = this.body.toString("utf-8");
|
|
323
|
+
const parsed = JSON.parse(raw);
|
|
324
|
+
const converted = convertHandlerBody(parsed);
|
|
325
|
+
this.#jsonCache = converted;
|
|
326
|
+
return converted;
|
|
327
|
+
}
|
|
328
|
+
form() {
|
|
329
|
+
if (this.#formCache !== void 0) {
|
|
330
|
+
return this.#formCache;
|
|
331
|
+
}
|
|
332
|
+
if (!this.body || this.body.length === 0) {
|
|
333
|
+
throw new Error("No body available to parse as form data");
|
|
334
|
+
}
|
|
335
|
+
const text = this.body.toString("utf-8");
|
|
336
|
+
const params = new URLSearchParams(text);
|
|
337
|
+
const form = {};
|
|
338
|
+
for (const [key, value] of params.entries()) {
|
|
339
|
+
form[key] = value;
|
|
340
|
+
}
|
|
341
|
+
this.#formCache = form;
|
|
342
|
+
return form;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
var normalizeHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
346
|
+
var extractQueryParams = (queryParams) => {
|
|
347
|
+
if (!queryParams || typeof queryParams !== "object") {
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
const result = {};
|
|
351
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
352
|
+
result[key] = String(value);
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
};
|
|
356
|
+
var convertBodyToBuffer = (body) => {
|
|
357
|
+
if (body === null || body === void 0) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(body)) {
|
|
361
|
+
if (body.length === 0 || body.every((b) => typeof b === "number")) {
|
|
362
|
+
return Buffer.from(body);
|
|
363
|
+
}
|
|
364
|
+
return Buffer.from(JSON.stringify(body), "utf-8");
|
|
365
|
+
}
|
|
366
|
+
if (typeof body === "object") {
|
|
367
|
+
return Buffer.from(JSON.stringify(body), "utf-8");
|
|
368
|
+
}
|
|
369
|
+
if (typeof body === "string") {
|
|
370
|
+
return Buffer.from(body, "utf-8");
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
};
|
|
374
|
+
function createRequest(data) {
|
|
375
|
+
return new RequestImpl(data);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/streaming.ts
|
|
379
|
+
var import_node_module = require("module");
|
|
380
|
+
var STREAM_HANDLE_PROP = "__spikard_stream_handle";
|
|
381
|
+
var nativeBinding = null;
|
|
382
|
+
var loadBinding = () => {
|
|
383
|
+
try {
|
|
384
|
+
const require2 = (0, import_node_module.createRequire)(importMetaUrl);
|
|
385
|
+
return require2("../index.js");
|
|
386
|
+
} catch {
|
|
387
|
+
console.warn("[spikard-node] Native binding not found. Please run: pnpm build:native");
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
nativeBinding = loadBinding();
|
|
392
|
+
var createHandle = (iterator, init) => {
|
|
393
|
+
if (nativeBinding && typeof nativeBinding.createStreamingHandle === "function") {
|
|
394
|
+
return { kind: "native", handle: nativeBinding.createStreamingHandle(iterator, init), init };
|
|
395
|
+
}
|
|
396
|
+
return { kind: "js", iterator, init };
|
|
397
|
+
};
|
|
398
|
+
var StreamingResponse = class {
|
|
399
|
+
[STREAM_HANDLE_PROP];
|
|
400
|
+
constructor(stream, init) {
|
|
401
|
+
const iterator = toAsyncIterator(stream);
|
|
402
|
+
this[STREAM_HANDLE_PROP] = createHandle(iterator, init ?? {});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function isStreamingResponse(value) {
|
|
406
|
+
return Boolean(value) && value instanceof StreamingResponse;
|
|
407
|
+
}
|
|
408
|
+
var getStreamingHandle = (response) => response[STREAM_HANDLE_PROP];
|
|
409
|
+
function toAsyncIterator(source) {
|
|
410
|
+
if (source && typeof source.next === "function") {
|
|
411
|
+
const iterator = source;
|
|
412
|
+
if (typeof iterator[Symbol.asyncIterator] === "function") {
|
|
413
|
+
return iterator;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
next: (...args) => iterator.next(...args),
|
|
417
|
+
[Symbol.asyncIterator]() {
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (source && typeof source[Symbol.asyncIterator] === "function") {
|
|
423
|
+
return source[Symbol.asyncIterator]();
|
|
424
|
+
}
|
|
425
|
+
throw new TypeError("StreamingResponse requires an async iterator or generator");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/handler-wrapper.ts
|
|
429
|
+
var NATIVE_HANDLER_FLAG = /* @__PURE__ */ Symbol("spikard.nativeHandler");
|
|
430
|
+
var formatHandlerResult = (result, objectMode) => {
|
|
431
|
+
if (objectMode) {
|
|
432
|
+
if (result !== void 0 && result !== null && typeof result === "object") {
|
|
433
|
+
if ("status" in result) {
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
if (isStreamingResponse(result)) {
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
return { status: 200, body: result };
|
|
440
|
+
}
|
|
441
|
+
return result === void 0 ? { status: 200, body: null } : { status: 200, body: result };
|
|
442
|
+
}
|
|
443
|
+
if (isStreamingResponse(result)) {
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
if (result === void 0) {
|
|
447
|
+
return "null";
|
|
448
|
+
}
|
|
449
|
+
if (typeof result === "string") {
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
return JSON.stringify(result);
|
|
453
|
+
};
|
|
454
|
+
var isNativeHandler = (handler) => Boolean(handler?.[NATIVE_HANDLER_FLAG]);
|
|
455
|
+
function markNative(handler) {
|
|
456
|
+
handler[NATIVE_HANDLER_FLAG] = true;
|
|
457
|
+
return handler;
|
|
458
|
+
}
|
|
459
|
+
function markRawBody(handler, prefersRaw) {
|
|
460
|
+
handler.__spikard_raw_body = prefersRaw;
|
|
461
|
+
return handler;
|
|
462
|
+
}
|
|
463
|
+
function prepareRequest(requestInput) {
|
|
464
|
+
let actualInput = requestInput;
|
|
465
|
+
if (Array.isArray(requestInput) && requestInput.length === 1) {
|
|
466
|
+
actualInput = requestInput[0];
|
|
467
|
+
}
|
|
468
|
+
const objectMode = typeof actualInput === "object" && actualInput !== null;
|
|
469
|
+
const data = objectMode ? actualInput : JSON.parse(actualInput);
|
|
470
|
+
const request = createRequest(data);
|
|
471
|
+
return { request, objectMode };
|
|
472
|
+
}
|
|
473
|
+
function wrapHandler(handler) {
|
|
474
|
+
const isHandlerAsync = handler.constructor.name === "AsyncFunction";
|
|
475
|
+
if (isHandlerAsync) {
|
|
476
|
+
const nativeHandler2 = async (requestInput) => {
|
|
477
|
+
const { request, objectMode } = prepareRequest(requestInput);
|
|
478
|
+
const result = await handler(request);
|
|
479
|
+
return formatHandlerResult(result, objectMode);
|
|
480
|
+
};
|
|
481
|
+
return markNative(markRawBody(nativeHandler2, true));
|
|
482
|
+
}
|
|
483
|
+
const nativeHandler = (requestInput) => {
|
|
484
|
+
const { request, objectMode } = prepareRequest(requestInput);
|
|
485
|
+
const result = handler(request);
|
|
486
|
+
return formatHandlerResult(result, objectMode);
|
|
487
|
+
};
|
|
488
|
+
return markNative(markRawBody(nativeHandler, true));
|
|
489
|
+
}
|
|
490
|
+
function wrapBodyHandler(handler) {
|
|
491
|
+
const isHandlerAsync = handler.constructor.name === "AsyncFunction";
|
|
492
|
+
if (isHandlerAsync) {
|
|
493
|
+
const nativeHandler2 = async (requestInput) => {
|
|
494
|
+
const { request, objectMode } = prepareRequest(requestInput);
|
|
495
|
+
const body = request.json();
|
|
496
|
+
const result = await handler(body, request);
|
|
497
|
+
return formatHandlerResult(result, objectMode);
|
|
498
|
+
};
|
|
499
|
+
return markNative(markRawBody(nativeHandler2, true));
|
|
500
|
+
}
|
|
501
|
+
const nativeHandler = (requestInput) => {
|
|
502
|
+
const { request, objectMode } = prepareRequest(requestInput);
|
|
503
|
+
const body = request.json();
|
|
504
|
+
const result = handler(body, request);
|
|
505
|
+
return formatHandlerResult(result, objectMode);
|
|
506
|
+
};
|
|
507
|
+
return markNative(markRawBody(nativeHandler, true));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/server.ts
|
|
511
|
+
var nativeBinding2;
|
|
512
|
+
function loadBinding2() {
|
|
513
|
+
try {
|
|
514
|
+
const require2 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
515
|
+
return require2("../index.js");
|
|
516
|
+
} catch {
|
|
517
|
+
console.warn("[spikard-node] Native binding not found. Please run: pnpm build:native");
|
|
518
|
+
return {
|
|
519
|
+
runServer: () => {
|
|
520
|
+
throw new Error("Native binding not built. Run: pnpm build:native");
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
nativeBinding2 = loadBinding2();
|
|
526
|
+
function runServer(app, config = {}) {
|
|
527
|
+
const handlers = {};
|
|
528
|
+
const routes = (app.routes || []).map((route2) => {
|
|
529
|
+
const handler = app.handlers?.[route2.handler_name];
|
|
530
|
+
if (!handler) return route2;
|
|
531
|
+
const nativeHandler = isNativeHandler(handler) ? handler : wrapHandler(handler);
|
|
532
|
+
handlers[route2.handler_name] = nativeHandler;
|
|
533
|
+
const isAsync = nativeHandler.constructor.name === "AsyncFunction";
|
|
534
|
+
return { ...route2, is_async: isAsync };
|
|
535
|
+
});
|
|
536
|
+
nativeBinding2.runServer({ ...app, handlers, routes }, config);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/app.ts
|
|
540
|
+
var ASYNC_GENERATOR_REGISTRY = /* @__PURE__ */ new Map();
|
|
541
|
+
var ASYNC_GENERATOR_COUNTER = 0;
|
|
542
|
+
var isAsyncGenerator = (value) => {
|
|
543
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
544
|
+
};
|
|
545
|
+
var normalizeDependencyKey = (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase();
|
|
546
|
+
var inferDependsOn = (factory) => {
|
|
547
|
+
const source = factory.toString();
|
|
548
|
+
const parenMatch = source.match(/^[^(]*\(([^)]*)\)/);
|
|
549
|
+
if (parenMatch) {
|
|
550
|
+
const rawParams = (parenMatch[1] ?? "").split(",").map((param) => param.trim()).filter((param) => param.length > 0);
|
|
551
|
+
return rawParams.filter((param) => !param.startsWith("{") && !param.startsWith("[")).map((param) => param.replace(/^\.{3}/, "")).map((param) => param.split("=")[0]?.trim() ?? "").filter((param) => param.length > 0).map((param) => normalizeDependencyKey(param));
|
|
552
|
+
}
|
|
553
|
+
const arrowMatch = source.match(/^\s*([A-Za-z0-9_$]+)\s*=>/);
|
|
554
|
+
if (arrowMatch) {
|
|
555
|
+
return [normalizeDependencyKey(arrowMatch[1] ?? "")].filter((param) => param.length > 0);
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
558
|
+
};
|
|
559
|
+
var parseDependencies = (payload) => {
|
|
560
|
+
if (typeof payload === "string") {
|
|
561
|
+
try {
|
|
562
|
+
const parsed = JSON.parse(payload);
|
|
563
|
+
return parsed ?? {};
|
|
564
|
+
} catch {
|
|
565
|
+
return {};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (typeof payload === "object" && payload !== null) {
|
|
569
|
+
return payload;
|
|
570
|
+
}
|
|
571
|
+
return {};
|
|
572
|
+
};
|
|
573
|
+
var wrapDependencyFactory = (factory) => {
|
|
574
|
+
return async (payload) => {
|
|
575
|
+
const deps = parseDependencies(payload);
|
|
576
|
+
const cleanupId = deps.__cleanup_id__;
|
|
577
|
+
if (typeof cleanupId === "string") {
|
|
578
|
+
const generator = ASYNC_GENERATOR_REGISTRY.get(cleanupId);
|
|
579
|
+
if (generator) {
|
|
580
|
+
ASYNC_GENERATOR_REGISTRY.delete(cleanupId);
|
|
581
|
+
if (typeof generator.return === "function") {
|
|
582
|
+
await generator.return(void 0);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return JSON.stringify({ ok: true });
|
|
586
|
+
}
|
|
587
|
+
const result = await factory(deps);
|
|
588
|
+
if (isAsyncGenerator(result)) {
|
|
589
|
+
const { value } = await result.next();
|
|
590
|
+
const id = `gen_${ASYNC_GENERATOR_COUNTER++}`;
|
|
591
|
+
ASYNC_GENERATOR_REGISTRY.set(id, result);
|
|
592
|
+
return JSON.stringify({ __async_generator__: true, value, cleanup_id: id });
|
|
593
|
+
}
|
|
594
|
+
return result === void 0 ? "null" : JSON.stringify(result);
|
|
595
|
+
};
|
|
596
|
+
};
|
|
597
|
+
var Spikard = class {
|
|
598
|
+
routes = [];
|
|
599
|
+
handlers = {};
|
|
600
|
+
websocketRoutes = [];
|
|
601
|
+
websocketHandlers = {};
|
|
602
|
+
lifecycleHooks = {
|
|
603
|
+
onRequest: [],
|
|
604
|
+
preValidation: [],
|
|
605
|
+
preHandler: [],
|
|
606
|
+
onResponse: [],
|
|
607
|
+
onError: []
|
|
608
|
+
};
|
|
609
|
+
dependencies = {};
|
|
610
|
+
/**
|
|
611
|
+
* Add a route to the application
|
|
612
|
+
*
|
|
613
|
+
* @param metadata - Route configuration metadata
|
|
614
|
+
* @param handler - Handler function (sync or async)
|
|
615
|
+
*/
|
|
616
|
+
addRoute(metadata, handler) {
|
|
617
|
+
this.routes.push(metadata);
|
|
618
|
+
this.handlers[metadata.handler_name] = handler;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Register a WebSocket route (message-based)
|
|
622
|
+
*/
|
|
623
|
+
websocket(path2, handler, options = {}) {
|
|
624
|
+
const handlerName = options.handlerName ?? `ws_${this.websocketRoutes.length}_${path2}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
625
|
+
const handlerWrapper = { handleMessage: handler };
|
|
626
|
+
if (options.messageSchema) {
|
|
627
|
+
handlerWrapper._messageSchema = options.messageSchema;
|
|
628
|
+
}
|
|
629
|
+
if (options.responseSchema) {
|
|
630
|
+
handlerWrapper._responseSchema = options.responseSchema;
|
|
631
|
+
}
|
|
632
|
+
const route2 = {
|
|
633
|
+
method: "GET",
|
|
634
|
+
path: path2,
|
|
635
|
+
handler_name: handlerName,
|
|
636
|
+
request_schema: options.messageSchema,
|
|
637
|
+
response_schema: options.responseSchema,
|
|
638
|
+
parameter_schema: void 0,
|
|
639
|
+
file_params: void 0,
|
|
640
|
+
is_async: true
|
|
641
|
+
};
|
|
642
|
+
this.websocketRoutes.push(route2);
|
|
643
|
+
this.websocketHandlers[handlerName] = handlerWrapper;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Run the server
|
|
647
|
+
*
|
|
648
|
+
* @param options - Server configuration
|
|
649
|
+
*/
|
|
650
|
+
run(options = {}) {
|
|
651
|
+
runServer(this, options);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Register an onRequest lifecycle hook
|
|
655
|
+
*
|
|
656
|
+
* Runs before routing. Can inspect/modify the request or short-circuit with a response.
|
|
657
|
+
*
|
|
658
|
+
* @param hook - Async function that receives a request and returns either:
|
|
659
|
+
* - The (possibly modified) request to continue processing
|
|
660
|
+
* - A Response object to short-circuit the request pipeline
|
|
661
|
+
* @returns The hook function (for decorator usage)
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```typescript
|
|
665
|
+
* app.onRequest(async (request) => {
|
|
666
|
+
* console.log(`Request: ${request.method} ${request.path}`);
|
|
667
|
+
* return request;
|
|
668
|
+
* });
|
|
669
|
+
* ```
|
|
670
|
+
*/
|
|
671
|
+
onRequest(hook) {
|
|
672
|
+
this.lifecycleHooks.onRequest.push(hook);
|
|
673
|
+
return hook;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Register a preValidation lifecycle hook
|
|
677
|
+
*
|
|
678
|
+
* Runs after routing but before validation. Useful for rate limiting.
|
|
679
|
+
*
|
|
680
|
+
* @param hook - Async function that receives a request and returns either:
|
|
681
|
+
* - The (possibly modified) request to continue processing
|
|
682
|
+
* - A Response object to short-circuit the request pipeline
|
|
683
|
+
* @returns The hook function (for decorator usage)
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* app.preValidation(async (request) => {
|
|
688
|
+
* if (tooManyRequests()) {
|
|
689
|
+
* return { error: "Rate limit exceeded", status: 429 };
|
|
690
|
+
* }
|
|
691
|
+
* return request;
|
|
692
|
+
* });
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
preValidation(hook) {
|
|
696
|
+
this.lifecycleHooks.preValidation.push(hook);
|
|
697
|
+
return hook;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Register a preHandler lifecycle hook
|
|
701
|
+
*
|
|
702
|
+
* Runs after validation but before the handler. Ideal for authentication/authorization.
|
|
703
|
+
*
|
|
704
|
+
* @param hook - Async function that receives a request and returns either:
|
|
705
|
+
* - The (possibly modified) request to continue processing
|
|
706
|
+
* - A Response object to short-circuit the request pipeline
|
|
707
|
+
* @returns The hook function (for decorator usage)
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```typescript
|
|
711
|
+
* app.preHandler(async (request) => {
|
|
712
|
+
* if (!validToken(request.headers.authorization)) {
|
|
713
|
+
* return { error: "Unauthorized", status: 401 };
|
|
714
|
+
* }
|
|
715
|
+
* return request;
|
|
716
|
+
* });
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
preHandler(hook) {
|
|
720
|
+
this.lifecycleHooks.preHandler.push(hook);
|
|
721
|
+
return hook;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Register an onResponse lifecycle hook
|
|
725
|
+
*
|
|
726
|
+
* Runs after the handler executes. Can modify the response.
|
|
727
|
+
*
|
|
728
|
+
* @param hook - Async function that receives a response and returns the (possibly modified) response
|
|
729
|
+
* @returns The hook function (for decorator usage)
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* app.onResponse(async (response) => {
|
|
734
|
+
* response.headers["X-Frame-Options"] = "DENY";
|
|
735
|
+
* return response;
|
|
736
|
+
* });
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
onResponse(hook) {
|
|
740
|
+
this.lifecycleHooks.onResponse.push(hook);
|
|
741
|
+
return hook;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Register an onError lifecycle hook
|
|
745
|
+
*
|
|
746
|
+
* Runs when an error occurs. Can customize error responses.
|
|
747
|
+
*
|
|
748
|
+
* @param hook - Async function that receives an error response and returns a (possibly modified) response
|
|
749
|
+
* @returns The hook function (for decorator usage)
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```typescript
|
|
753
|
+
* app.onError(async (response) => {
|
|
754
|
+
* response.headers["Content-Type"] = "application/json";
|
|
755
|
+
* return response;
|
|
756
|
+
* });
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
onError(hook) {
|
|
760
|
+
this.lifecycleHooks.onError.push(hook);
|
|
761
|
+
return hook;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Register a dependency value or factory
|
|
765
|
+
*
|
|
766
|
+
* Provides a value or factory function to be injected into handlers.
|
|
767
|
+
* Dependencies are matched by parameter name or can be accessed via the
|
|
768
|
+
* request context.
|
|
769
|
+
*
|
|
770
|
+
* @param key - Unique identifier for the dependency
|
|
771
|
+
* @param valueOrFactory - Static value or factory function
|
|
772
|
+
* @param options - Configuration options for the dependency
|
|
773
|
+
* @returns The Spikard instance for method chaining
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```typescript
|
|
777
|
+
* // Simple value dependency
|
|
778
|
+
* app.provide('app_name', 'MyApp');
|
|
779
|
+
* app.provide('max_connections', 100);
|
|
780
|
+
*
|
|
781
|
+
* // Factory dependency
|
|
782
|
+
* app.provide('db_pool', async ({ database_url }) => {
|
|
783
|
+
* return await createPool(database_url);
|
|
784
|
+
* }, { dependsOn: ['database_url'], singleton: true });
|
|
785
|
+
*
|
|
786
|
+
* // Use in handler
|
|
787
|
+
* app.get('/config', async (request, { app_name, db_pool }) => {
|
|
788
|
+
* return { app: app_name, pool: db_pool };
|
|
789
|
+
* });
|
|
790
|
+
* ```
|
|
791
|
+
*/
|
|
792
|
+
provide(key, valueOrFactory, options) {
|
|
793
|
+
const normalizedKey = normalizeDependencyKey(key);
|
|
794
|
+
const isFactory = typeof valueOrFactory === "function";
|
|
795
|
+
const factory = isFactory ? wrapDependencyFactory(valueOrFactory) : void 0;
|
|
796
|
+
const explicitDependsOn = options?.dependsOn;
|
|
797
|
+
const inferredDependsOn = isFactory && explicitDependsOn === void 0 ? inferDependsOn(valueOrFactory) : [];
|
|
798
|
+
const dependsOn = (explicitDependsOn ?? inferredDependsOn).map((depKey) => normalizeDependencyKey(depKey));
|
|
799
|
+
this.dependencies[normalizedKey] = {
|
|
800
|
+
isFactory,
|
|
801
|
+
value: isFactory ? void 0 : valueOrFactory,
|
|
802
|
+
factory,
|
|
803
|
+
dependsOn,
|
|
804
|
+
singleton: options?.singleton ?? false,
|
|
805
|
+
cacheable: options?.cacheable ?? !isFactory
|
|
806
|
+
};
|
|
807
|
+
return this;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Get all registered lifecycle hooks
|
|
811
|
+
*
|
|
812
|
+
* @returns Dictionary of hook lists by type
|
|
813
|
+
*/
|
|
814
|
+
getLifecycleHooks() {
|
|
815
|
+
return {
|
|
816
|
+
onRequest: [...this.lifecycleHooks.onRequest ?? []],
|
|
817
|
+
preValidation: [...this.lifecycleHooks.preValidation ?? []],
|
|
818
|
+
preHandler: [...this.lifecycleHooks.preHandler ?? []],
|
|
819
|
+
onResponse: [...this.lifecycleHooks.onResponse ?? []],
|
|
820
|
+
onError: [...this.lifecycleHooks.onError ?? []]
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// src/background.ts
|
|
826
|
+
var background_exports = {};
|
|
827
|
+
__export(background_exports, {
|
|
828
|
+
run: () => run
|
|
829
|
+
});
|
|
830
|
+
var import__ = require("../index.js");
|
|
831
|
+
function run(work) {
|
|
832
|
+
const task = async () => {
|
|
833
|
+
await work();
|
|
834
|
+
return void 0;
|
|
835
|
+
};
|
|
836
|
+
(0, import__.backgroundRun)(task);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/grpc.ts
|
|
840
|
+
var GrpcStatusCode = /* @__PURE__ */ ((GrpcStatusCode2) => {
|
|
841
|
+
GrpcStatusCode2[GrpcStatusCode2["OK"] = 0] = "OK";
|
|
842
|
+
GrpcStatusCode2[GrpcStatusCode2["CANCELLED"] = 1] = "CANCELLED";
|
|
843
|
+
GrpcStatusCode2[GrpcStatusCode2["UNKNOWN"] = 2] = "UNKNOWN";
|
|
844
|
+
GrpcStatusCode2[GrpcStatusCode2["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
|
|
845
|
+
GrpcStatusCode2[GrpcStatusCode2["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
|
|
846
|
+
GrpcStatusCode2[GrpcStatusCode2["NOT_FOUND"] = 5] = "NOT_FOUND";
|
|
847
|
+
GrpcStatusCode2[GrpcStatusCode2["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
|
|
848
|
+
GrpcStatusCode2[GrpcStatusCode2["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
|
|
849
|
+
GrpcStatusCode2[GrpcStatusCode2["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
|
|
850
|
+
GrpcStatusCode2[GrpcStatusCode2["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
|
|
851
|
+
GrpcStatusCode2[GrpcStatusCode2["ABORTED"] = 10] = "ABORTED";
|
|
852
|
+
GrpcStatusCode2[GrpcStatusCode2["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
|
|
853
|
+
GrpcStatusCode2[GrpcStatusCode2["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
|
|
854
|
+
GrpcStatusCode2[GrpcStatusCode2["INTERNAL"] = 13] = "INTERNAL";
|
|
855
|
+
GrpcStatusCode2[GrpcStatusCode2["UNAVAILABLE"] = 14] = "UNAVAILABLE";
|
|
856
|
+
GrpcStatusCode2[GrpcStatusCode2["DATA_LOSS"] = 15] = "DATA_LOSS";
|
|
857
|
+
GrpcStatusCode2[GrpcStatusCode2["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
|
|
858
|
+
return GrpcStatusCode2;
|
|
859
|
+
})(GrpcStatusCode || {});
|
|
860
|
+
var GrpcError = class extends Error {
|
|
861
|
+
/**
|
|
862
|
+
* gRPC status code
|
|
863
|
+
*/
|
|
864
|
+
code;
|
|
865
|
+
/**
|
|
866
|
+
* Create a new gRPC error
|
|
867
|
+
*
|
|
868
|
+
* @param code - gRPC status code
|
|
869
|
+
* @param message - Error message
|
|
870
|
+
*/
|
|
871
|
+
constructor(code, message) {
|
|
872
|
+
super(message);
|
|
873
|
+
this.code = code;
|
|
874
|
+
this.name = "GrpcError";
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
function createUnaryHandler(methodName, handler, requestType, responseType) {
|
|
878
|
+
return {
|
|
879
|
+
async handleRequest(request) {
|
|
880
|
+
if (request.methodName !== methodName) {
|
|
881
|
+
throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
|
|
882
|
+
}
|
|
883
|
+
const req = requestType.decode(request.payload);
|
|
884
|
+
const result = await handler(req, request.metadata);
|
|
885
|
+
let response;
|
|
886
|
+
let responseMetadata;
|
|
887
|
+
if (result && typeof result === "object" && "response" in result) {
|
|
888
|
+
response = result.response;
|
|
889
|
+
responseMetadata = result.metadata;
|
|
890
|
+
} else {
|
|
891
|
+
response = result;
|
|
892
|
+
}
|
|
893
|
+
const encoded = responseType.encode(response).finish();
|
|
894
|
+
if (responseMetadata) {
|
|
895
|
+
return {
|
|
896
|
+
payload: Buffer.from(encoded),
|
|
897
|
+
metadata: responseMetadata
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
payload: Buffer.from(encoded)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function createServiceHandler(methods) {
|
|
907
|
+
return {
|
|
908
|
+
async handleRequest(request) {
|
|
909
|
+
const handler = methods[request.methodName];
|
|
910
|
+
if (!handler) {
|
|
911
|
+
throw new GrpcError(12 /* UNIMPLEMENTED */, `Method ${request.methodName} not implemented`);
|
|
912
|
+
}
|
|
913
|
+
return handler.handleRequest(request);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/routing.ts
|
|
919
|
+
function route(path2, options = {}) {
|
|
920
|
+
return (handler) => {
|
|
921
|
+
const methods = options.methods ? Array.isArray(options.methods) ? options.methods : [options.methods] : ["GET"];
|
|
922
|
+
const metadata = {
|
|
923
|
+
method: methods.join(","),
|
|
924
|
+
path: path2,
|
|
925
|
+
handler_name: handler.name || "anonymous",
|
|
926
|
+
request_schema: options.bodySchema ?? void 0,
|
|
927
|
+
response_schema: options.responseSchema ?? void 0,
|
|
928
|
+
parameter_schema: options.parameterSchema ?? void 0,
|
|
929
|
+
cors: options.cors ?? void 0,
|
|
930
|
+
is_async: true
|
|
931
|
+
};
|
|
932
|
+
handler.__route_metadata__ = metadata;
|
|
933
|
+
return handler;
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function get(path2, options = {}) {
|
|
937
|
+
return route(path2, { ...options, methods: ["GET"] });
|
|
938
|
+
}
|
|
939
|
+
function post(path2, options = {}) {
|
|
940
|
+
return route(path2, { ...options, methods: ["POST"] });
|
|
941
|
+
}
|
|
942
|
+
function put(path2, options = {}) {
|
|
943
|
+
return route(path2, { ...options, methods: ["PUT"] });
|
|
944
|
+
}
|
|
945
|
+
function del(path2, options = {}) {
|
|
946
|
+
return route(path2, { ...options, methods: ["DELETE"] });
|
|
947
|
+
}
|
|
948
|
+
function patch(path2, options = {}) {
|
|
949
|
+
return route(path2, { ...options, methods: ["PATCH"] });
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/testing.ts
|
|
953
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
954
|
+
var import_node_module3 = require("module");
|
|
955
|
+
var import_node_path = __toESM(require("path"));
|
|
956
|
+
var import_node_zlib = require("zlib");
|
|
957
|
+
var MockWebSocketConnection = class {
|
|
958
|
+
handler;
|
|
959
|
+
queue = [];
|
|
960
|
+
constructor(handler) {
|
|
961
|
+
this.handler = handler;
|
|
962
|
+
}
|
|
963
|
+
async send_json(message) {
|
|
964
|
+
const response = await this.handler(message);
|
|
965
|
+
this.queue.push(response);
|
|
966
|
+
}
|
|
967
|
+
async sendJson(message) {
|
|
968
|
+
return this.send_json(message);
|
|
969
|
+
}
|
|
970
|
+
async sendText(text) {
|
|
971
|
+
return this.send_json(text);
|
|
972
|
+
}
|
|
973
|
+
async receive_json() {
|
|
974
|
+
return this.queue.shift();
|
|
975
|
+
}
|
|
976
|
+
async receiveJson() {
|
|
977
|
+
return this.receive_json();
|
|
978
|
+
}
|
|
979
|
+
async receiveText() {
|
|
980
|
+
const value = await this.receive_json();
|
|
981
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
982
|
+
}
|
|
983
|
+
async receiveBytes() {
|
|
984
|
+
const value = await this.receive_json();
|
|
985
|
+
if (Buffer.isBuffer(value)) {
|
|
986
|
+
return value;
|
|
987
|
+
}
|
|
988
|
+
if (typeof value === "string") {
|
|
989
|
+
return Buffer.from(value);
|
|
990
|
+
}
|
|
991
|
+
return Buffer.from(JSON.stringify(value));
|
|
992
|
+
}
|
|
993
|
+
async receiveMessage() {
|
|
994
|
+
return this.receive_json();
|
|
995
|
+
}
|
|
996
|
+
async close() {
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
var nativeTestClient = null;
|
|
1000
|
+
var loadNativeTestClient = () => {
|
|
1001
|
+
try {
|
|
1002
|
+
const require2 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
1003
|
+
const binding = require2("../index.js");
|
|
1004
|
+
return binding.TestClient;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
nativeTestClient = loadNativeTestClient();
|
|
1010
|
+
var isNativeCtor = nativeTestClient !== null;
|
|
1011
|
+
var JsTestResponse = class {
|
|
1012
|
+
constructor(statusCode, headers, body) {
|
|
1013
|
+
this.statusCode = statusCode;
|
|
1014
|
+
this.headerBag = headers;
|
|
1015
|
+
this.body = body;
|
|
1016
|
+
}
|
|
1017
|
+
body;
|
|
1018
|
+
headerBag;
|
|
1019
|
+
headers() {
|
|
1020
|
+
return this.headerBag;
|
|
1021
|
+
}
|
|
1022
|
+
text() {
|
|
1023
|
+
return this.decodeBody().toString("utf-8");
|
|
1024
|
+
}
|
|
1025
|
+
json() {
|
|
1026
|
+
const raw = this.text();
|
|
1027
|
+
if (raw.length === 0) {
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
return JSON.parse(raw);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return raw;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
bytes() {
|
|
1037
|
+
return this.decodeBody();
|
|
1038
|
+
}
|
|
1039
|
+
decodeBody() {
|
|
1040
|
+
const encoding = (this.headerBag["content-encoding"] ?? "").toLowerCase();
|
|
1041
|
+
if (encoding === "gzip") {
|
|
1042
|
+
try {
|
|
1043
|
+
return (0, import_node_zlib.gunzipSync)(this.body);
|
|
1044
|
+
} catch {
|
|
1045
|
+
return this.body;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return this.body;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Extract GraphQL data from response
|
|
1052
|
+
*
|
|
1053
|
+
* @returns The data field from GraphQL response, or null/undefined if not present
|
|
1054
|
+
*/
|
|
1055
|
+
graphqlData() {
|
|
1056
|
+
const response = this.json();
|
|
1057
|
+
return typeof response === "object" && response !== null && "data" in response ? response.data : void 0;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Extract GraphQL errors from response
|
|
1061
|
+
*
|
|
1062
|
+
* @returns Array of GraphQL error objects, or empty array if none present
|
|
1063
|
+
*/
|
|
1064
|
+
graphqlErrors() {
|
|
1065
|
+
const response = this.json();
|
|
1066
|
+
return typeof response === "object" && response !== null && Array.isArray(response.errors) ? response.errors : [];
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
var JsNativeClient = class {
|
|
1070
|
+
routes;
|
|
1071
|
+
handlers;
|
|
1072
|
+
dependencies;
|
|
1073
|
+
config;
|
|
1074
|
+
rateLimitBuckets;
|
|
1075
|
+
websocketRoutes;
|
|
1076
|
+
websocketHandlers;
|
|
1077
|
+
constructor(routesJson, _websocketRoutesJson, handlers, websocketHandlers, dependencies, _lifecycleHooks, config) {
|
|
1078
|
+
this.routes = JSON.parse(routesJson);
|
|
1079
|
+
this.websocketRoutes = JSON.parse(_websocketRoutesJson ?? "[]");
|
|
1080
|
+
this.handlers = handlers;
|
|
1081
|
+
this.websocketHandlers = websocketHandlers;
|
|
1082
|
+
this.dependencies = dependencies;
|
|
1083
|
+
this.config = config;
|
|
1084
|
+
this.rateLimitBuckets = /* @__PURE__ */ new Map();
|
|
1085
|
+
}
|
|
1086
|
+
matchRoute(method, path2) {
|
|
1087
|
+
const cleanedPath = path2.split("?")[0] ?? path2;
|
|
1088
|
+
for (const route2 of this.routes) {
|
|
1089
|
+
if (route2.method.toUpperCase() !== method.toUpperCase()) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const params = this.extractParams(route2.path, cleanedPath);
|
|
1093
|
+
if (params) {
|
|
1094
|
+
return { handlerName: route2.handler_name, params, route: route2 };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
throw new Error(`No route matched ${method} ${path2}`);
|
|
1098
|
+
}
|
|
1099
|
+
extractParams(pattern, actual) {
|
|
1100
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
1101
|
+
const actualParts = actual.split("/").filter(Boolean);
|
|
1102
|
+
const hasTailParam = patternParts.some((part) => part.includes(":path"));
|
|
1103
|
+
if (!hasTailParam && patternParts.length !== actualParts.length) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
if (hasTailParam && actualParts.length < patternParts.length - 1) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
const params = {};
|
|
1110
|
+
for (let i = 0; i < patternParts.length; i += 1) {
|
|
1111
|
+
const patternPart = patternParts[i];
|
|
1112
|
+
const actualPart = actualParts[i];
|
|
1113
|
+
if (!patternPart || !actualPart) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
if (patternPart.startsWith(":") || patternPart.startsWith("{") && patternPart.endsWith("}")) {
|
|
1117
|
+
const isPathTailParam = patternPart.includes(":path");
|
|
1118
|
+
const rawKey = patternPart.startsWith("{") ? patternPart.slice(1, -1).split(":")[0] : patternPart.slice(1);
|
|
1119
|
+
const key = rawKey ?? "";
|
|
1120
|
+
if (isPathTailParam) {
|
|
1121
|
+
params[key] = decodeURIComponent(actualParts.slice(i).join("/"));
|
|
1122
|
+
return params;
|
|
1123
|
+
}
|
|
1124
|
+
params[key] = decodeURIComponent(actualPart);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (patternPart !== actualPart) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return params;
|
|
1132
|
+
}
|
|
1133
|
+
buildQuery(path2) {
|
|
1134
|
+
const query = {};
|
|
1135
|
+
const url = new URL(path2, "http://localhost");
|
|
1136
|
+
url.searchParams.forEach((value, key) => {
|
|
1137
|
+
if (!(key in query)) {
|
|
1138
|
+
query[key] = value;
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
return query;
|
|
1142
|
+
}
|
|
1143
|
+
validateParams(route2, params) {
|
|
1144
|
+
const schema = route2.parameter_schema;
|
|
1145
|
+
if (!schema?.properties) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
for (const [key, meta] of Object.entries(schema.properties)) {
|
|
1149
|
+
if (meta.source !== "path") {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const raw = params[key];
|
|
1153
|
+
if (raw === void 0) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (meta.format === "date" || meta.format === "date-time") {
|
|
1157
|
+
const parsed = new Date(raw);
|
|
1158
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
1159
|
+
return new JsTestResponse(422, {}, Buffer.from(""));
|
|
1160
|
+
}
|
|
1161
|
+
params[key] = parsed;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
async invoke(method, path2, headers, body) {
|
|
1167
|
+
const routeMatch = (() => {
|
|
1168
|
+
try {
|
|
1169
|
+
return this.matchRoute(method, path2);
|
|
1170
|
+
} catch {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
})();
|
|
1174
|
+
if (!routeMatch) {
|
|
1175
|
+
const staticResponse = await this.serveStatic(path2);
|
|
1176
|
+
if (staticResponse) {
|
|
1177
|
+
return staticResponse;
|
|
1178
|
+
}
|
|
1179
|
+
throw new Error(`No route matched ${method} ${path2}`);
|
|
1180
|
+
}
|
|
1181
|
+
const { handlerName, params, route: route2 } = routeMatch;
|
|
1182
|
+
const handler = this.handlers[handlerName];
|
|
1183
|
+
if (!handler) {
|
|
1184
|
+
throw new Error(`Handler not found for ${handlerName}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (this.config?.rateLimit && this.isRateLimited(route2.path)) {
|
|
1187
|
+
return new JsTestResponse(429, {}, Buffer.from(""));
|
|
1188
|
+
}
|
|
1189
|
+
const validationResponse = this.validateParams(route2, params);
|
|
1190
|
+
if (validationResponse) {
|
|
1191
|
+
return validationResponse;
|
|
1192
|
+
}
|
|
1193
|
+
const requestPayload = {
|
|
1194
|
+
method,
|
|
1195
|
+
path: path2,
|
|
1196
|
+
params,
|
|
1197
|
+
query: this.buildQuery(path2),
|
|
1198
|
+
headers: headers ?? {},
|
|
1199
|
+
cookies: {},
|
|
1200
|
+
body: this.encodeBody(body),
|
|
1201
|
+
dependencies: this.dependencies ?? void 0
|
|
1202
|
+
};
|
|
1203
|
+
const result = await handler(this.safeStringify(requestPayload));
|
|
1204
|
+
const response = await this.toResponse(result);
|
|
1205
|
+
return this.applyCompression(response, headers);
|
|
1206
|
+
}
|
|
1207
|
+
async toResponse(result) {
|
|
1208
|
+
if (isStreamingResponse(result)) {
|
|
1209
|
+
const handle = getStreamingHandle(result);
|
|
1210
|
+
if (handle.kind === "js") {
|
|
1211
|
+
const buffers = [];
|
|
1212
|
+
for await (const chunk of handle.iterator) {
|
|
1213
|
+
if (chunk === null || chunk === void 0) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (Buffer.isBuffer(chunk)) {
|
|
1217
|
+
buffers.push(chunk);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (typeof chunk === "string") {
|
|
1221
|
+
buffers.push(Buffer.from(chunk));
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
|
|
1225
|
+
const view = ArrayBuffer.isView(chunk) ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.from(chunk);
|
|
1226
|
+
buffers.push(view);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
buffers.push(Buffer.from(this.safeStringify(chunk)));
|
|
1230
|
+
}
|
|
1231
|
+
const bodyBuffer = buffers.length === 0 ? Buffer.alloc(0) : Buffer.concat(buffers);
|
|
1232
|
+
return new JsTestResponse(handle.init.statusCode ?? 200, handle.init.headers ?? {}, bodyBuffer);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (typeof result === "string") {
|
|
1236
|
+
try {
|
|
1237
|
+
const parsed = JSON.parse(result);
|
|
1238
|
+
if (parsed && typeof parsed === "object" && ("status" in parsed || "statusCode" in parsed || "body" in parsed)) {
|
|
1239
|
+
const statusCode = parsed.status ?? parsed.statusCode ?? 200;
|
|
1240
|
+
const textBody = typeof parsed.body === "string" || parsed.body === void 0 ? parsed.body ?? "" : JSON.stringify(parsed.body);
|
|
1241
|
+
return new JsTestResponse(statusCode, parsed.headers ?? {}, Buffer.from(textBody));
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
return new JsTestResponse(200, {}, Buffer.from(result));
|
|
1246
|
+
}
|
|
1247
|
+
if (result && typeof result === "object" && ("status" in result || "statusCode" in result)) {
|
|
1248
|
+
const payload = result;
|
|
1249
|
+
const statusCode = payload.status ?? payload.statusCode ?? 200;
|
|
1250
|
+
const textBody = typeof payload.body === "string" || payload.body === void 0 ? payload.body ?? "" : JSON.stringify(payload.body);
|
|
1251
|
+
return new JsTestResponse(statusCode, payload.headers ?? {}, Buffer.from(textBody));
|
|
1252
|
+
}
|
|
1253
|
+
const text = this.safeStringify(result);
|
|
1254
|
+
return new JsTestResponse(200, {}, Buffer.from(text));
|
|
1255
|
+
}
|
|
1256
|
+
isRateLimited(routePath) {
|
|
1257
|
+
if (!this.config?.rateLimit) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1261
|
+
const key = routePath;
|
|
1262
|
+
const existing = this.rateLimitBuckets.get(key);
|
|
1263
|
+
const bucket = existing && existing.resetAt === now ? existing : { tokens: this.config.rateLimit.burst, resetAt: now };
|
|
1264
|
+
if (bucket.tokens <= 0) {
|
|
1265
|
+
this.rateLimitBuckets.set(key, bucket);
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
bucket.tokens -= 1;
|
|
1269
|
+
this.rateLimitBuckets.set(key, bucket);
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
async serveStatic(targetPath) {
|
|
1273
|
+
const normalized = targetPath.split("?")[0] ?? targetPath;
|
|
1274
|
+
const staticConfig = this.config?.staticFiles ?? [];
|
|
1275
|
+
for (const entry of staticConfig) {
|
|
1276
|
+
if (!normalized.startsWith(entry.routePrefix)) {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const relative = normalized.slice(entry.routePrefix.length);
|
|
1280
|
+
const resolved = relative === "/" || relative === "" ? "index.html" : relative.replace(/^\//, "");
|
|
1281
|
+
const filePath = import_node_path.default.join(entry.directory, resolved);
|
|
1282
|
+
try {
|
|
1283
|
+
const contents = await import_promises.default.readFile(filePath);
|
|
1284
|
+
const contentType = this.detectContentType(filePath);
|
|
1285
|
+
const headers = {
|
|
1286
|
+
"content-type": contentType
|
|
1287
|
+
};
|
|
1288
|
+
if (entry.cacheControl) {
|
|
1289
|
+
headers["cache-control"] = entry.cacheControl;
|
|
1290
|
+
}
|
|
1291
|
+
const bodyBuffer = contentType.startsWith("text/") ? Buffer.from(contents.toString("utf-8").replace(/\n$/, "")) : contents;
|
|
1292
|
+
return new JsTestResponse(200, headers, bodyBuffer);
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
detectContentType(filePath) {
|
|
1299
|
+
const ext = import_node_path.default.extname(filePath).toLowerCase();
|
|
1300
|
+
switch (ext) {
|
|
1301
|
+
case ".txt":
|
|
1302
|
+
return "text/plain";
|
|
1303
|
+
case ".html":
|
|
1304
|
+
case ".htm":
|
|
1305
|
+
return "text/html";
|
|
1306
|
+
case ".json":
|
|
1307
|
+
return "application/json";
|
|
1308
|
+
case ".xml":
|
|
1309
|
+
return "application/xml";
|
|
1310
|
+
case ".csv":
|
|
1311
|
+
return "text/csv";
|
|
1312
|
+
case ".png":
|
|
1313
|
+
return "image/png";
|
|
1314
|
+
case ".jpg":
|
|
1315
|
+
case ".jpeg":
|
|
1316
|
+
return "image/jpeg";
|
|
1317
|
+
case ".pdf":
|
|
1318
|
+
return "application/pdf";
|
|
1319
|
+
default:
|
|
1320
|
+
return "application/octet-stream";
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
applyCompression(response, requestHeaders) {
|
|
1324
|
+
const config = this.config?.compression;
|
|
1325
|
+
const acceptsGzip = (this.lookupHeader(requestHeaders, "accept-encoding") ?? "").includes("gzip");
|
|
1326
|
+
if (!config || !config.gzip || !acceptsGzip) {
|
|
1327
|
+
return response;
|
|
1328
|
+
}
|
|
1329
|
+
const rawBody = response.bytes();
|
|
1330
|
+
if (rawBody.length < (config.minSize ?? 0)) {
|
|
1331
|
+
return response;
|
|
1332
|
+
}
|
|
1333
|
+
const gzipped = (0, import_node_zlib.gzipSync)(rawBody, { level: config.quality ?? 6 });
|
|
1334
|
+
const headers = { ...response.headers(), "content-encoding": "gzip" };
|
|
1335
|
+
return new JsTestResponse(response.statusCode, headers, gzipped);
|
|
1336
|
+
}
|
|
1337
|
+
lookupHeader(headers, name) {
|
|
1338
|
+
if (!headers) {
|
|
1339
|
+
return void 0;
|
|
1340
|
+
}
|
|
1341
|
+
const target = name.toLowerCase();
|
|
1342
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1343
|
+
if (key.toLowerCase() === target) {
|
|
1344
|
+
return value;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return void 0;
|
|
1348
|
+
}
|
|
1349
|
+
safeStringify(value) {
|
|
1350
|
+
return JSON.stringify(value, (_key, val) => {
|
|
1351
|
+
if (typeof val === "bigint") {
|
|
1352
|
+
return val.toString();
|
|
1353
|
+
}
|
|
1354
|
+
return val;
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
encodeBody(body) {
|
|
1358
|
+
if (body === null) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
if (typeof body === "object" && ("__spikard_multipart__" in body || "__spikard_form__" in body)) {
|
|
1362
|
+
return Array.from(Buffer.from(this.safeStringify(body)));
|
|
1363
|
+
}
|
|
1364
|
+
if (Buffer.isBuffer(body)) {
|
|
1365
|
+
return Array.from(body);
|
|
1366
|
+
}
|
|
1367
|
+
return body;
|
|
1368
|
+
}
|
|
1369
|
+
async get(path2, headers) {
|
|
1370
|
+
return this.invoke("GET", path2, headers, null);
|
|
1371
|
+
}
|
|
1372
|
+
async post(path2, headers, body) {
|
|
1373
|
+
return this.invoke("POST", path2, headers, body);
|
|
1374
|
+
}
|
|
1375
|
+
async put(path2, headers, body) {
|
|
1376
|
+
return this.invoke("PUT", path2, headers, body);
|
|
1377
|
+
}
|
|
1378
|
+
async delete(path2, headers) {
|
|
1379
|
+
return this.invoke("DELETE", path2, headers, null);
|
|
1380
|
+
}
|
|
1381
|
+
async patch(path2, headers, body) {
|
|
1382
|
+
return this.invoke("PATCH", path2, headers, body);
|
|
1383
|
+
}
|
|
1384
|
+
async head(path2, headers) {
|
|
1385
|
+
return this.invoke("HEAD", path2, headers, null);
|
|
1386
|
+
}
|
|
1387
|
+
async options(path2, headers) {
|
|
1388
|
+
return this.invoke("OPTIONS", path2, headers, null);
|
|
1389
|
+
}
|
|
1390
|
+
async trace(path2, headers) {
|
|
1391
|
+
return this.invoke("TRACE", path2, headers, null);
|
|
1392
|
+
}
|
|
1393
|
+
async websocket(_path) {
|
|
1394
|
+
const match = this.websocketRoutes?.find((route2) => route2.path === _path);
|
|
1395
|
+
if (!match) {
|
|
1396
|
+
throw new Error("WebSocket testing is not available in the JS fallback client");
|
|
1397
|
+
}
|
|
1398
|
+
const handlerEntry = this.websocketHandlers?.[match.handler_name];
|
|
1399
|
+
if (!handlerEntry) {
|
|
1400
|
+
throw new Error("WebSocket testing is not available in the JS fallback client");
|
|
1401
|
+
}
|
|
1402
|
+
const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
|
|
1403
|
+
if (!handler) {
|
|
1404
|
+
throw new Error("WebSocket testing is not available in the JS fallback client");
|
|
1405
|
+
}
|
|
1406
|
+
const mock = new MockWebSocketConnection(async (msg) => handler(msg));
|
|
1407
|
+
return mock;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
var defaultNativeClientFactory = (routesJson, websocketRoutesJson, handlers, websocketHandlers, dependencies, lifecycleHooks, config) => {
|
|
1411
|
+
if (isNativeCtor && nativeTestClient) {
|
|
1412
|
+
return new nativeTestClient(
|
|
1413
|
+
routesJson,
|
|
1414
|
+
websocketRoutesJson,
|
|
1415
|
+
handlers,
|
|
1416
|
+
websocketHandlers,
|
|
1417
|
+
dependencies,
|
|
1418
|
+
lifecycleHooks,
|
|
1419
|
+
config
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
return new JsNativeClient(
|
|
1423
|
+
routesJson,
|
|
1424
|
+
websocketRoutesJson,
|
|
1425
|
+
handlers,
|
|
1426
|
+
websocketHandlers,
|
|
1427
|
+
dependencies,
|
|
1428
|
+
lifecycleHooks,
|
|
1429
|
+
config
|
|
1430
|
+
);
|
|
1431
|
+
};
|
|
1432
|
+
var nativeClientFactory = defaultNativeClientFactory;
|
|
1433
|
+
var TestClient = class {
|
|
1434
|
+
app;
|
|
1435
|
+
nativeClient;
|
|
1436
|
+
looksLikeStringHandler(fn) {
|
|
1437
|
+
const source = fn.toString();
|
|
1438
|
+
return source.includes("requestJson") || source.includes("request_json") || source.includes("JSON.parse") || source.includes("JSON.parse(");
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Create a new test client
|
|
1442
|
+
*
|
|
1443
|
+
* @param app - Spikard application with routes and handlers
|
|
1444
|
+
*/
|
|
1445
|
+
constructor(app) {
|
|
1446
|
+
if (!app || !Array.isArray(app.routes)) {
|
|
1447
|
+
throw new Error("Invalid Spikard app: missing routes array");
|
|
1448
|
+
}
|
|
1449
|
+
this.app = app;
|
|
1450
|
+
const routesJson = JSON.stringify(app.routes);
|
|
1451
|
+
const websocketRoutesJson = JSON.stringify(app.websocketRoutes ?? []);
|
|
1452
|
+
const handlerEntries = Object.entries(app.handlers || {});
|
|
1453
|
+
const handlersMap = Object.fromEntries(
|
|
1454
|
+
handlerEntries.map(([name, handler]) => {
|
|
1455
|
+
const prefersParsedBody = this.looksLikeStringHandler(handler);
|
|
1456
|
+
const nativeHandler = isNativeHandler(handler) || prefersParsedBody ? handler : wrapHandler(handler);
|
|
1457
|
+
if (prefersParsedBody) {
|
|
1458
|
+
nativeHandler.__spikard_raw_body = false;
|
|
1459
|
+
} else if (nativeHandler.__spikard_raw_body === void 0) {
|
|
1460
|
+
nativeHandler.__spikard_raw_body = true;
|
|
1461
|
+
}
|
|
1462
|
+
return [name, nativeHandler];
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1465
|
+
const websocketHandlersMap = app.websocketHandlers || {};
|
|
1466
|
+
const config = app.config ?? null;
|
|
1467
|
+
const dependencies = app.dependencies ?? null;
|
|
1468
|
+
const lifecycleHooks = app.getLifecycleHooks?.() ?? null;
|
|
1469
|
+
this.nativeClient = nativeClientFactory(
|
|
1470
|
+
routesJson,
|
|
1471
|
+
websocketRoutesJson,
|
|
1472
|
+
handlersMap,
|
|
1473
|
+
websocketHandlersMap,
|
|
1474
|
+
dependencies,
|
|
1475
|
+
lifecycleHooks,
|
|
1476
|
+
config
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Make a GET request
|
|
1481
|
+
*
|
|
1482
|
+
* @param path - Request path
|
|
1483
|
+
* @param headers - Optional request headers
|
|
1484
|
+
* @returns Response promise
|
|
1485
|
+
*/
|
|
1486
|
+
async get(path2, headers) {
|
|
1487
|
+
return this.nativeClient.get(path2, headers || null);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Make a POST request
|
|
1491
|
+
*
|
|
1492
|
+
* @param path - Request path
|
|
1493
|
+
* @param options - Request options
|
|
1494
|
+
* @returns Response promise
|
|
1495
|
+
*/
|
|
1496
|
+
async post(path2, options) {
|
|
1497
|
+
return this.nativeClient.post(path2, this.buildHeaders(options), this.buildBody(options));
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Make a PUT request
|
|
1501
|
+
*
|
|
1502
|
+
* @param path - Request path
|
|
1503
|
+
* @param options - Request options
|
|
1504
|
+
* @returns Response promise
|
|
1505
|
+
*/
|
|
1506
|
+
async put(path2, options) {
|
|
1507
|
+
return this.nativeClient.put(path2, this.buildHeaders(options), this.buildBody(options));
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Make a DELETE request
|
|
1511
|
+
*
|
|
1512
|
+
* @param path - Request path
|
|
1513
|
+
* @param headers - Optional request headers
|
|
1514
|
+
* @returns Response promise
|
|
1515
|
+
*/
|
|
1516
|
+
async delete(path2, headers) {
|
|
1517
|
+
return this.nativeClient.delete(path2, headers || null);
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Make a PATCH request
|
|
1521
|
+
*
|
|
1522
|
+
* @param path - Request path
|
|
1523
|
+
* @param options - Request options
|
|
1524
|
+
* @returns Response promise
|
|
1525
|
+
*/
|
|
1526
|
+
async patch(path2, options) {
|
|
1527
|
+
return this.nativeClient.patch(path2, this.buildHeaders(options), this.buildBody(options));
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Make a HEAD request
|
|
1531
|
+
*
|
|
1532
|
+
* @param path - Request path
|
|
1533
|
+
* @param headers - Optional request headers
|
|
1534
|
+
* @returns Response promise
|
|
1535
|
+
*/
|
|
1536
|
+
async head(path2, headers) {
|
|
1537
|
+
return this.nativeClient.head(path2, headers || null);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Make an OPTIONS request
|
|
1541
|
+
*
|
|
1542
|
+
* @param path - Request path
|
|
1543
|
+
* @param options - Request options
|
|
1544
|
+
* @returns Response promise
|
|
1545
|
+
*/
|
|
1546
|
+
async options(path2, options) {
|
|
1547
|
+
return this.nativeClient.options(path2, this.buildHeaders(options));
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Make a TRACE request
|
|
1551
|
+
*
|
|
1552
|
+
* @param path - Request path
|
|
1553
|
+
* @param headers - Optional request headers
|
|
1554
|
+
* @returns Response promise
|
|
1555
|
+
*/
|
|
1556
|
+
async trace(path2, options) {
|
|
1557
|
+
return this.nativeClient.trace(path2, this.buildHeaders(options));
|
|
1558
|
+
}
|
|
1559
|
+
buildHeaders(options) {
|
|
1560
|
+
if (!options?.headers || Object.keys(options.headers).length === 0) {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
return options.headers;
|
|
1564
|
+
}
|
|
1565
|
+
buildBody(options) {
|
|
1566
|
+
if (!options) {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
if (options.multipart) {
|
|
1570
|
+
return {
|
|
1571
|
+
__spikard_multipart__: {
|
|
1572
|
+
fields: options.multipart.fields ?? {},
|
|
1573
|
+
files: options.multipart.files ?? []
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
if (options.form) {
|
|
1578
|
+
return {
|
|
1579
|
+
__spikard_form__: options.form
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
if ("json" in options) {
|
|
1583
|
+
return options.json ?? null;
|
|
1584
|
+
}
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Connect to a WebSocket endpoint
|
|
1589
|
+
*
|
|
1590
|
+
* Uses the native test client to create an in-memory WebSocket connection.
|
|
1591
|
+
*
|
|
1592
|
+
* @param path - WebSocket path
|
|
1593
|
+
* @returns WebSocket test connection
|
|
1594
|
+
*/
|
|
1595
|
+
async websocketConnect(path2) {
|
|
1596
|
+
const handlerName = this.app.websocketRoutes?.find((r) => r.path === path2)?.handler_name;
|
|
1597
|
+
const handlerEntry = handlerName ? this.app.websocketHandlers?.[handlerName] : void 0;
|
|
1598
|
+
const handler = handlerEntry && typeof handlerEntry.handleMessage === "function" ? handlerEntry.handleMessage : null;
|
|
1599
|
+
if (handler) {
|
|
1600
|
+
const mock = new MockWebSocketConnection(async (msg) => handler(msg));
|
|
1601
|
+
return mock;
|
|
1602
|
+
}
|
|
1603
|
+
const routeMatch = this.app.routes.find((r) => r.path === path2);
|
|
1604
|
+
if (routeMatch) {
|
|
1605
|
+
const handlerFn = this.app.handlers?.[routeMatch.handler_name];
|
|
1606
|
+
if (handlerFn) {
|
|
1607
|
+
const mock = new MockWebSocketConnection(async (msg) => {
|
|
1608
|
+
const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
1609
|
+
const result = await handlerFn(payload);
|
|
1610
|
+
if (typeof result === "string") {
|
|
1611
|
+
try {
|
|
1612
|
+
return JSON.parse(result);
|
|
1613
|
+
} catch {
|
|
1614
|
+
return result;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return result;
|
|
1618
|
+
});
|
|
1619
|
+
return mock;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return this.nativeClient.websocket(path2);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Send a GraphQL query/mutation
|
|
1626
|
+
*
|
|
1627
|
+
* Convenience method for sending GraphQL queries and mutations.
|
|
1628
|
+
*
|
|
1629
|
+
* @param query - GraphQL query string
|
|
1630
|
+
* @param variables - Optional GraphQL variables object
|
|
1631
|
+
* @param operationName - Optional GraphQL operation name
|
|
1632
|
+
* @returns Response promise
|
|
1633
|
+
*
|
|
1634
|
+
* @example
|
|
1635
|
+
* ```typescript
|
|
1636
|
+
* const response = await client.graphql('query { user(id: "1") { id name } }');
|
|
1637
|
+
* const user = response.graphqlData().user;
|
|
1638
|
+
* ```
|
|
1639
|
+
*/
|
|
1640
|
+
async graphql(query, variables, operationName) {
|
|
1641
|
+
const json = { query };
|
|
1642
|
+
if (variables !== null && variables !== void 0) {
|
|
1643
|
+
json.variables = variables;
|
|
1644
|
+
}
|
|
1645
|
+
if (operationName !== null && operationName !== void 0) {
|
|
1646
|
+
json.operationName = operationName;
|
|
1647
|
+
}
|
|
1648
|
+
return this.post("/graphql", { json });
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Send a GraphQL query and get HTTP status separately
|
|
1652
|
+
*
|
|
1653
|
+
* Returns status information alongside the response for cases where
|
|
1654
|
+
* you need both the HTTP status and the full response details.
|
|
1655
|
+
*
|
|
1656
|
+
* @param query - GraphQL query string
|
|
1657
|
+
* @param variables - Optional GraphQL variables object
|
|
1658
|
+
* @param operationName - Optional GraphQL operation name
|
|
1659
|
+
* @returns Promise with status and response details
|
|
1660
|
+
*/
|
|
1661
|
+
async graphqlWithStatus(query, variables, operationName) {
|
|
1662
|
+
const response = await this.graphql(query, variables, operationName);
|
|
1663
|
+
return {
|
|
1664
|
+
status: response.statusCode,
|
|
1665
|
+
statusCode: response.statusCode,
|
|
1666
|
+
headers: JSON.stringify(response.headers()),
|
|
1667
|
+
bodyText: response.text()
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Cleanup resources when test client is done
|
|
1672
|
+
*/
|
|
1673
|
+
async cleanup() {
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1677
|
+
0 && (module.exports = {
|
|
1678
|
+
GrpcError,
|
|
1679
|
+
GrpcStatusCode,
|
|
1680
|
+
Spikard,
|
|
1681
|
+
StreamingResponse,
|
|
1682
|
+
TestClient,
|
|
1683
|
+
UploadFile,
|
|
1684
|
+
background,
|
|
1685
|
+
createServiceHandler,
|
|
1686
|
+
createUnaryHandler,
|
|
1687
|
+
del,
|
|
1688
|
+
get,
|
|
1689
|
+
patch,
|
|
1690
|
+
post,
|
|
1691
|
+
put,
|
|
1692
|
+
route,
|
|
1693
|
+
runServer,
|
|
1694
|
+
wrapBodyHandler,
|
|
1695
|
+
wrapHandler
|
|
1696
|
+
});
|
|
1697
|
+
//# sourceMappingURL=index.js.map
|