arri 0.12.8 → 0.15.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.
@@ -1,4 +1,4 @@
1
- import { createError, isError, setResponseStatus, send, eventHandler, isPreflightRequest, readRawBody, getValidatedQuery, setResponseHeader, defineEventHandler, getQuery, setHeader, createRouter, createApp, sendError } from 'h3';
1
+ import { createError, isError, setResponseStatus, send, eventHandler, isPreflightRequest, setResponseHeader, readRawBody, getValidatedQuery, setHeaders, getHeader, sendStream, defineEventHandler, getQuery, setHeader, createRouter, createApp, sendError } from 'h3';
2
2
  import { a, isAObjectSchema, isARecordSchema, isADiscriminatorSchema } from 'arri-validate';
3
3
  import { isRpcHttpMethod, removeDisallowedChars } from 'arri-codegen-utils';
4
4
  import { kebabCase, pascalCase } from 'scule';
@@ -208,28 +208,25 @@ function isRpcParamSchema(input) {
208
208
  return isAObjectSchema(input) || isARecordSchema(input) || isADiscriminatorSchema(input);
209
209
  }
210
210
  function isRpc(input) {
211
- if (typeof input !== "object" || input === null) {
212
- return false;
213
- }
214
- const anyInput = input;
215
- if (!isRpcHttpMethod(anyInput.method)) {
216
- return false;
217
- }
218
- if (typeof anyInput.handler !== "function") {
219
- return false;
220
- }
221
- return true;
211
+ return typeof input === "object" && input !== null && "method" in input && isRpcHttpMethod(input.method) && "handler" in input && typeof input.handler === "function";
222
212
  }
223
213
  function defineRpc(config) {
224
214
  return config;
225
215
  }
226
216
  function createRpcDefinition(rpcName, httpPath, procedure) {
217
+ let method;
218
+ if (procedure.isEventStream === true) {
219
+ method = procedure.method ?? "get";
220
+ } else {
221
+ method = procedure.method ?? "post";
222
+ }
227
223
  return {
228
224
  description: procedure.description,
229
225
  path: httpPath,
230
- method: procedure.method ?? "post",
226
+ method,
231
227
  params: getRpcParamName(rpcName, procedure),
232
- response: getRpcResponseDefinition(rpcName, procedure)
228
+ response: getRpcResponseDefinition(rpcName, procedure),
229
+ isEventStream: procedure.isEventStream === true ? true : void 0
233
230
  };
234
231
  }
235
232
  function getRpcPath(rpcName, prefix = "") {
@@ -295,70 +292,11 @@ function registerRpc(router, path, procedure, opts) {
295
292
  }
296
293
  }
297
294
  if (isRpcParamSchema(procedure.params)) {
298
- switch (httpMethod) {
299
- case "get": {
300
- const parsedParams = await getValidatedQuery(
301
- event,
302
- (input) => a.safeCoerce(procedure.params, input)
303
- );
304
- if (parsedParams.success) {
305
- event.context.params = parsedParams.value;
306
- } else {
307
- const errParts = [];
308
- for (const err of parsedParams.error.errors) {
309
- const errPath = err.instancePath.split("/");
310
- errPath.shift();
311
- const propName = errPath.join(".");
312
- if (!errParts.includes(propName)) {
313
- errParts.push(propName);
314
- }
315
- }
316
- const message = `Missing or invalid url query parameters: [${errParts.join(
317
- ", "
318
- )}]`;
319
- throw defineError(400, {
320
- statusMessage: message,
321
- data: parsedParams.error
322
- });
323
- }
324
- break;
325
- }
326
- case "delete":
327
- case "patch":
328
- case "post":
329
- case "put": {
330
- const body = await readRawBody(event);
331
- if (!body) {
332
- throw defineError(400, {
333
- statusMessage: `Invalid request body. Expected object. Got undefined.`
334
- });
335
- }
336
- const parsedParams = a.safeParse(
337
- procedure.params,
338
- body
339
- );
340
- if (!parsedParams.success) {
341
- const errorParts = [];
342
- for (const err of parsedParams.error.errors) {
343
- const errPath = err.instancePath.split("/");
344
- errPath.shift();
345
- if (!errorParts.includes(errPath.join("."))) {
346
- errorParts.push(errPath.join("."));
347
- }
348
- }
349
- throw defineError(400, {
350
- statusMessage: `Invalid request body. Affected properties [${errorParts.join(
351
- ", "
352
- )}]`,
353
- data: parsedParams.error
354
- });
355
- }
356
- event.context.params = parsedParams.value;
357
- break;
358
- }
359
- default:
360
- break;
361
- }
295
+ await validateRpcRequestInput(
296
+ event,
297
+ httpMethod,
298
+ procedure.params
299
+ );
362
300
  }
363
301
  const response = await procedure.handler(
364
302
  event.context,
@@ -408,29 +346,297 @@ function registerRpc(router, path, procedure, opts) {
408
346
  break;
409
347
  }
410
348
  }
349
+ async function validateRpcRequestInput(event, httpMethod, schema) {
350
+ switch (httpMethod) {
351
+ case "get": {
352
+ const parsedParams = await getValidatedQuery(
353
+ event,
354
+ (input) => a.safeCoerce(schema, input)
355
+ );
356
+ if (parsedParams.success) {
357
+ event.context.params = parsedParams.value;
358
+ } else {
359
+ const errParts = [];
360
+ for (const err of parsedParams.error.errors) {
361
+ const errPath = err.instancePath.split("/");
362
+ errPath.shift();
363
+ const propName = errPath.join(".");
364
+ if (!errParts.includes(propName)) {
365
+ errParts.push(propName);
366
+ }
367
+ }
368
+ const message = `Missing or invalid url query parameters: [${errParts.join(
369
+ ", "
370
+ )}]`;
371
+ throw defineError(400, {
372
+ statusMessage: message,
373
+ data: parsedParams.error
374
+ });
375
+ }
376
+ break;
377
+ }
378
+ case "delete":
379
+ case "patch":
380
+ case "post":
381
+ case "put": {
382
+ const body = await readRawBody(event);
383
+ if (!body) {
384
+ throw defineError(400, {
385
+ statusMessage: `Invalid request body. Expected object. Got undefined.`
386
+ });
387
+ }
388
+ const parsedParams = a.safeParse(schema, body);
389
+ if (!parsedParams.success) {
390
+ const errorParts = [];
391
+ for (const err of parsedParams.error.errors) {
392
+ const errPath = err.instancePath.split("/");
393
+ errPath.shift();
394
+ if (!errorParts.includes(errPath.join("."))) {
395
+ errorParts.push(errPath.join("."));
396
+ }
397
+ }
398
+ throw defineError(400, {
399
+ statusMessage: `Invalid request body. Affected properties [${errorParts.join(
400
+ ", "
401
+ )}]`,
402
+ data: parsedParams.error
403
+ });
404
+ }
405
+ event.context.params = parsedParams.value;
406
+ break;
407
+ }
408
+ }
409
+ }
411
410
 
412
- var __defProp$1 = Object.defineProperty;
413
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
414
- var __publicField$1 = (obj, key, value) => {
415
- __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
411
+ var __defProp$2 = Object.defineProperty;
412
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
413
+ var __publicField$2 = (obj, key, value) => {
414
+ __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
416
415
  return value;
417
416
  };
418
- class ArriRouter {
419
- constructor() {
420
- __publicField$1(this, "procedures", []);
421
- __publicField$1(this, "routes", []);
417
+ function setSseHeaders(event) {
418
+ setHeaders(event, {
419
+ "Transfer-Encoding": "chunked",
420
+ "Content-Type": "text/event-stream",
421
+ Connection: "keep-alive",
422
+ "Cache-Control": "no-cache"
423
+ });
424
+ }
425
+ function defineEventStreamRpc(config) {
426
+ return {
427
+ ...config,
428
+ method: config.method ?? "get",
429
+ isEventStream: true
430
+ };
431
+ }
432
+ function isEventStreamRpc(input) {
433
+ return isRpc(input) && "isEventStream" in input && input.isEventStream === true;
434
+ }
435
+ function formatSse({ id, data, event }) {
436
+ const parts = [];
437
+ if (id) {
438
+ parts.push(`id: ${id}`);
422
439
  }
423
- rpc(procedure) {
424
- this.procedures.push(procedure);
440
+ if (event) {
441
+ parts.push(`event: ${event}`);
425
442
  }
426
- route(route) {
427
- this.routes.push(route);
443
+ parts.push(`data: ${data}`);
444
+ const payload = `${parts.join("\n")}
445
+
446
+ `;
447
+ return payload;
448
+ }
449
+ function formatSseList(events) {
450
+ let output = "";
451
+ for (const event of events) {
452
+ output += formatSse(event);
428
453
  }
429
- getProcedures() {
430
- return this.procedures;
454
+ return output;
455
+ }
456
+ class EventStreamConnection {
457
+ constructor(event, opts) {
458
+ __publicField$2(this, "lastEventId");
459
+ __publicField$2(this, "writable");
460
+ __publicField$2(this, "readable");
461
+ __publicField$2(this, "writer");
462
+ __publicField$2(this, "encoder");
463
+ __publicField$2(this, "serializer");
464
+ __publicField$2(this, "h3Event");
465
+ __publicField$2(this, "pingInterval");
466
+ __publicField$2(this, "pingIntervalMs");
467
+ this.h3Event = event;
468
+ setSseHeaders(this.h3Event);
469
+ setResponseStatus(this.h3Event, 200);
470
+ const id = getHeader(event, "Last-Event-ID");
471
+ this.lastEventId = id;
472
+ const { readable, writable } = new TransformStream();
473
+ this.writable = writable;
474
+ this.readable = readable;
475
+ this.writer = writable.getWriter();
476
+ this.encoder = new TextEncoder();
477
+ this.pingIntervalMs = opts.pingInterval ?? 6e4;
478
+ this.serializer = opts.serializer;
431
479
  }
432
- getRoutes() {
433
- return this.routes;
480
+ /**
481
+ * Start sending the event stream to the client
482
+ */
483
+ start() {
484
+ this.h3Event._handled = true;
485
+ void sendStream(this.h3Event, this.readable);
486
+ this.pingInterval = setInterval(async () => {
487
+ await this.publishEvent({
488
+ id: this.lastEventId,
489
+ event: "ping",
490
+ data: ""
491
+ });
492
+ }, this.pingIntervalMs);
493
+ }
494
+ async push(data, eventId) {
495
+ if (Array.isArray(data)) {
496
+ const events = [];
497
+ for (const item of data) {
498
+ events.push({
499
+ id: eventId,
500
+ event: "message",
501
+ data: this.serializer(item)
502
+ });
503
+ }
504
+ await this.publishEvents(events);
505
+ return;
506
+ }
507
+ await this.publishEvent({
508
+ id: eventId,
509
+ event: "message",
510
+ data: this.serializer(data)
511
+ });
512
+ }
513
+ // /**
514
+ // * Push a custom event. These events will need to be parsed manually using the `onEvent` hooks of any generated clients.
515
+ // * Note events with the name "error" or "message" cannot be used for custom events.
516
+ // */
517
+ // async pushCustomEvent(event: SseEvent): Promise<void> {
518
+ // if (event.event === "message") {
519
+ // throw new Error(
520
+ // `Event type "message" is the default event type. Therefore it cannot be used when pushing custom events.`,
521
+ // );
522
+ // }
523
+ // if (event.event === "error") {
524
+ // throw new Error(
525
+ // `Event type "error" is reserved for the pushError() method. Therefore it cannot be used when pushing custom events.`,
526
+ // );
527
+ // }
528
+ // }
529
+ /**
530
+ * Publish an error event. This will trigger the `onError` hooks of any connected clients.
531
+ */
532
+ async pushError(error, eventId) {
533
+ await this.publishEvent({
534
+ id: eventId,
535
+ event: "error",
536
+ data: JSON.stringify(error)
537
+ });
538
+ }
539
+ async publishEvents(events) {
540
+ const payload = formatSseList(events);
541
+ await this.writer.write(this.encoder.encode(payload));
542
+ }
543
+ async publishEvent(event) {
544
+ const payload = formatSse(event);
545
+ await this.writer.write(this.encoder.encode(payload));
546
+ }
547
+ async cleanup() {
548
+ if (this.pingInterval) {
549
+ clearInterval(this.pingInterval);
550
+ }
551
+ try {
552
+ await this.writer.close();
553
+ } catch (_) {
554
+ }
555
+ }
556
+ /**
557
+ * Tell clients that the stream has ended and close the connection.
558
+ */
559
+ async end() {
560
+ await this.publishEvent({
561
+ event: "done",
562
+ data: "this stream has ended"
563
+ }).catch();
564
+ this.h3Event.node.res.end();
565
+ await this.cleanup();
566
+ }
567
+ on(event, callback) {
568
+ switch (event) {
569
+ case "disconnect":
570
+ this.h3Event.node.req.on("close", async () => {
571
+ await callback();
572
+ await this.cleanup();
573
+ });
574
+ break;
575
+ case "end":
576
+ this.h3Event.node.req.on("end", callback);
577
+ break;
578
+ }
579
+ }
580
+ }
581
+ function registerEventStreamRpc(router, path, procedure, opts) {
582
+ let responseValidator;
583
+ try {
584
+ responseValidator = procedure.response ? a.compile(procedure.response) : void 0;
585
+ } catch (err) {
586
+ console.error("ERROR COMPILING VALIDATOR", err);
587
+ }
588
+ const httpMethod = procedure.method ?? "get";
589
+ const handler = eventHandler(async (event) => {
590
+ event.context.rpcName = procedure.name;
591
+ if (isPreflightRequest(event)) {
592
+ return "ok";
593
+ }
594
+ try {
595
+ if (opts.onRequest) {
596
+ await opts.onRequest(event);
597
+ }
598
+ if (opts.middleware.length) {
599
+ for (const m of opts.middleware) {
600
+ await m(event);
601
+ }
602
+ }
603
+ if (isRpcParamSchema(procedure.params)) {
604
+ await validateRpcRequestInput(
605
+ event,
606
+ httpMethod,
607
+ procedure.params
608
+ );
609
+ }
610
+ const connection = new EventStreamConnection(event, {
611
+ pingInterval: procedure.pingInterval,
612
+ serializer: responseValidator?.serialize ?? function(_) {
613
+ return "";
614
+ }
615
+ });
616
+ event.context.connection = connection;
617
+ await procedure.handler(event.context, event);
618
+ } catch (err) {
619
+ await handleH3Error(err, event, opts.onError);
620
+ }
621
+ return "";
622
+ });
623
+ switch (httpMethod) {
624
+ case "get":
625
+ router.get(path, handler);
626
+ break;
627
+ case "delete":
628
+ router.delete(path, handler);
629
+ break;
630
+ case "patch":
631
+ router.patch(path, handler);
632
+ break;
633
+ case "put":
634
+ router.put(path, handler);
635
+ break;
636
+ case "post":
637
+ default:
638
+ router.post(path, handler);
639
+ break;
434
640
  }
435
641
  }
436
642
 
@@ -555,6 +761,40 @@ function handleRoute(router, method, route, opts) {
555
761
  }
556
762
  }
557
763
 
764
+ var __defProp$1 = Object.defineProperty;
765
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
766
+ var __publicField$1 = (obj, key, value) => {
767
+ __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
768
+ return value;
769
+ };
770
+ class ArriRouter {
771
+ constructor() {
772
+ __publicField$1(this, "procedures", []);
773
+ __publicField$1(this, "routes", []);
774
+ __publicField$1(this, "models", {});
775
+ }
776
+ rpc(procedure) {
777
+ this.procedures.push(procedure);
778
+ }
779
+ route(route) {
780
+ this.routes.push(route);
781
+ }
782
+ registerModels(models) {
783
+ for (const key of Object.keys(models)) {
784
+ this.models[key] = models[key];
785
+ }
786
+ }
787
+ getProcedures() {
788
+ return this.procedures;
789
+ }
790
+ getRoutes() {
791
+ return this.routes;
792
+ }
793
+ getModels() {
794
+ return this.models;
795
+ }
796
+ }
797
+
558
798
  var __defProp = Object.defineProperty;
559
799
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
560
800
  var __publicField = (obj, key, value) => {
@@ -608,11 +848,6 @@ class ArriApp {
608
848
  if (this.onRequest) {
609
849
  await this.onRequest(event);
610
850
  }
611
- if (this.middlewares.length) {
612
- for (const m of this.middlewares) {
613
- await m(event);
614
- }
615
- }
616
851
  } catch (err) {
617
852
  await handleH3Error(err, event, this.onError);
618
853
  }
@@ -637,6 +872,7 @@ class ArriApp {
637
872
  for (const rpc of input.getProcedures()) {
638
873
  this.rpc(rpc);
639
874
  }
875
+ this.registerModels(input.getModels());
640
876
  return;
641
877
  }
642
878
  this.middlewares.push(input);
@@ -660,6 +896,16 @@ class ArriApp {
660
896
  this.models[responseName] = procedure.response;
661
897
  }
662
898
  }
899
+ if (isEventStreamRpc(procedure)) {
900
+ registerEventStreamRpc(this.h3Router, path, procedure, {
901
+ middleware: this.middlewares,
902
+ onRequest: this.onRequest,
903
+ onError: this.onError,
904
+ onAfterResponse: this.onAfterResponse,
905
+ onBeforeResponse: this.onBeforeResponse
906
+ });
907
+ return;
908
+ }
663
909
  registerRpc(this.h3Router, path, procedure, {
664
910
  middleware: this.middlewares,
665
911
  onRequest: this.onRequest,
@@ -677,6 +923,11 @@ class ArriApp {
677
923
  onBeforeResponse: this.onBeforeResponse
678
924
  });
679
925
  }
926
+ registerModels(models) {
927
+ for (const key of Object.keys(models)) {
928
+ this.models[key] = models[key];
929
+ }
930
+ }
680
931
  getAppDefinition() {
681
932
  const appDef = {
682
933
  arriSchemaVersion: "0.0.2",
@@ -713,7 +964,7 @@ const defaultConfig = {
713
964
  procedureGlobPatterns: ["**/*.rpc.ts"],
714
965
  clientGenerators: [],
715
966
  buildDir: ".arri",
716
- buildEntry: "",
967
+ serverEntry: "",
717
968
  esbuild: {}
718
969
  };
719
970
  function defineConfig(config) {
@@ -727,8 +978,8 @@ function defineConfig(config) {
727
978
  clientGenerators: config.clientGenerators ?? defaultConfig.clientGenerators,
728
979
  buildDir: config.buildDir ?? defaultConfig.buildDir,
729
980
  esbuild: config.esbuild ?? defaultConfig.esbuild,
730
- buildEntry: config.buildEntry ?? ""
981
+ serverEntry: config.serverEntry ?? ""
731
982
  };
732
983
  }
733
984
 
734
- export { ArriApp as A, DEV_ENDPOINT_ROOT as D, ErrorResponse as E, DEV_DEFINITION_ENDPOINT as a, isRpc as b, createRpcDefinition as c, defineRpc as d, getRpcParamName as e, getRpcResponseName as f, getRpcPath as g, defineError as h, isRpcParamSchema as i, errorResponseFromValidationErrors as j, handleH3Error as k, isArriConfig as l, isResolvedArriConfig as m, defaultConfig as n, defineConfig as o, ArriRouter as p, defineRoute as q, registerRpc as r, registerRoute as s, handleRoute as t };
985
+ export { ArriApp as A, EventStreamConnection as B, registerEventStreamRpc as C, DEV_ENDPOINT_ROOT as D, ErrorResponse as E, DEV_DEFINITION_ENDPOINT as a, isRpc as b, createRpcDefinition as c, defineRpc as d, getRpcParamName as e, getRpcResponseName as f, getRpcPath as g, defineError as h, isRpcParamSchema as i, errorResponseFromValidationErrors as j, handleH3Error as k, isArriConfig as l, isResolvedArriConfig as m, defaultConfig as n, defineConfig as o, ArriRouter as p, defineRoute as q, registerRpc as r, registerRoute as s, handleRoute as t, setSseHeaders as u, validateRpcRequestInput as v, defineEventStreamRpc as w, isEventStreamRpc as x, formatSse as y, formatSseList as z };