@reboot-dev/reboot 0.28.0 → 0.29.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/index.d.ts +14 -11
- package/index.js +64 -34
- package/package.json +9 -4
- package/rbt-schema-to-proto.d.ts +2 -0
- package/rbt-schema-to-proto.js +597 -0
- package/reboot_native.cc +3 -3
- package/reboot_native.cjs +1 -1
- package/reboot_native.d.ts +1 -1
- package/secrets/index.d.ts +1 -0
- package/version.d.ts +1 -1
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { errors_pb, IdempotencyOptions, protobuf_es, ScheduleOptions } from "@reboot-dev/reboot-api";
|
|
1
|
+
import { errors_pb, IdempotencyOptions, protobuf_es, ScheduleOptions, tasks_pb } from "@reboot-dev/reboot-api";
|
|
2
|
+
import { z } from "zod/v4";
|
|
2
3
|
import * as reboot_native from "./reboot_native.cjs";
|
|
3
4
|
import { Application as ExpressApplication, NextFunction as ExpressNextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express";
|
|
4
5
|
export { reboot_native };
|
|
@@ -160,8 +161,8 @@ export declare abstract class AuthorizerRule<StateType, RequestType> {
|
|
|
160
161
|
request?: RequestType;
|
|
161
162
|
}): Promise<AuthorizerDecision>;
|
|
162
163
|
}
|
|
163
|
-
export declare function deny(): AuthorizerRule<
|
|
164
|
-
export declare function allow(): AuthorizerRule<
|
|
164
|
+
export declare function deny(): AuthorizerRule<any, any>;
|
|
165
|
+
export declare function allow(): AuthorizerRule<any, any>;
|
|
165
166
|
export declare function allowIf<StateType, RequestType>(args: {
|
|
166
167
|
all: AuthorizerCallable<StateType, RequestType>[];
|
|
167
168
|
any?: never;
|
|
@@ -172,13 +173,13 @@ export declare function allowIf<StateType, RequestType>(args: {
|
|
|
172
173
|
}): AuthorizerRule<StateType, RequestType>;
|
|
173
174
|
export declare function hasVerifiedToken({ context, }: {
|
|
174
175
|
context: ReaderContext;
|
|
175
|
-
state?:
|
|
176
|
-
request?:
|
|
176
|
+
state?: any;
|
|
177
|
+
request?: any;
|
|
177
178
|
}): errors_pb.Unauthenticated | errors_pb.Ok;
|
|
178
179
|
export declare function isAppInternal({ context, }: {
|
|
179
180
|
context: ReaderContext;
|
|
180
|
-
state?:
|
|
181
|
-
request?:
|
|
181
|
+
state?: any;
|
|
182
|
+
request?: any;
|
|
182
183
|
}): errors_pb.PermissionDenied | errors_pb.Ok;
|
|
183
184
|
export declare class Application {
|
|
184
185
|
#private;
|
|
@@ -213,10 +214,6 @@ export declare namespace Application {
|
|
|
213
214
|
post(path: Http.Path, ...handlers: Http.Handler[]): void;
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
|
-
/**
|
|
217
|
-
* @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
|
|
218
|
-
*/
|
|
219
|
-
export declare function testWithReboot(name: string, body: (t: any, rbt: Reboot) => Promise<void>): void;
|
|
220
217
|
export declare class Loop {
|
|
221
218
|
when: Date;
|
|
222
219
|
constructor(options?: ScheduleOptions);
|
|
@@ -265,3 +262,9 @@ export declare function until<T>(idempotencyAlias: string, context: WorkflowCont
|
|
|
265
262
|
parse?: undefined;
|
|
266
263
|
validate: (result: T) => boolean;
|
|
267
264
|
}): Promise<Exclude<T, boolean>>;
|
|
265
|
+
export declare const zod: {
|
|
266
|
+
tasks: {
|
|
267
|
+
TaskId: z.ZodCustom<protobuf_es.PartialMessage<tasks_pb.TaskId>, protobuf_es.PartialMessage<tasks_pb.TaskId>>;
|
|
268
|
+
};
|
|
269
|
+
bytes: z.ZodCustom<Uint8Array, Uint8Array>;
|
|
270
|
+
};
|
package/index.js
CHANGED
|
@@ -10,12 +10,12 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
12
|
var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
|
|
13
|
-
import { auth_pb, errors_pb, protobuf_es, } from "@reboot-dev/reboot-api";
|
|
13
|
+
import { auth_pb, errors_pb, protobuf_es, tasks_pb, } from "@reboot-dev/reboot-api";
|
|
14
14
|
import { strict as assert } from "assert";
|
|
15
15
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
16
|
import { fork } from "node:child_process";
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
|
-
import {
|
|
18
|
+
import { z } from "zod/v4";
|
|
19
19
|
import * as reboot_native from "./reboot_native.cjs";
|
|
20
20
|
import { ensureError } from "./utils/errors.js";
|
|
21
21
|
import { ensurePythonVenv } from "./venv.js";
|
|
@@ -613,22 +613,6 @@ _Application_servicers = new WeakMap(), _Application_tokenVerifier = new WeakMap
|
|
|
613
613
|
_Http_express = new WeakMap(), _Http_createExternalContext = new WeakMap();
|
|
614
614
|
Application.Http = Http;
|
|
615
615
|
})(Application || (Application = {}));
|
|
616
|
-
/**
|
|
617
|
-
* @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
|
|
618
|
-
*/
|
|
619
|
-
export function testWithReboot(name, body) {
|
|
620
|
-
console.warn("testWithReboot is deprecated and will be removed in the future.");
|
|
621
|
-
test(name, async (t) => {
|
|
622
|
-
const rbt = new Reboot();
|
|
623
|
-
await rbt.start();
|
|
624
|
-
try {
|
|
625
|
-
await body(t, rbt);
|
|
626
|
-
}
|
|
627
|
-
finally {
|
|
628
|
-
await rbt.stop();
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
616
|
export class Loop {
|
|
633
617
|
constructor(options) {
|
|
634
618
|
this.when = options?.when || new Date();
|
|
@@ -658,13 +642,20 @@ export async function retry_reactively_until(context, condition) {
|
|
|
658
642
|
});
|
|
659
643
|
return t;
|
|
660
644
|
}
|
|
661
|
-
async function
|
|
645
|
+
async function memoize(idempotencyAlias, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, until = false, }) {
|
|
662
646
|
assert(stringify !== undefined);
|
|
663
647
|
assert(parse !== undefined);
|
|
664
648
|
assert(atMostOnce !== undefined);
|
|
665
|
-
const result = await reboot_native.
|
|
649
|
+
const result = await reboot_native.memoize(context.__external, idempotencyAlias, async () => {
|
|
666
650
|
const t = await callable();
|
|
667
651
|
if (t !== undefined) {
|
|
652
|
+
if (validate === undefined) {
|
|
653
|
+
// TODO: link to docs about why this is required, when those docs exist.
|
|
654
|
+
throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
|
|
655
|
+
}
|
|
656
|
+
else if (!validate(t)) {
|
|
657
|
+
throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
|
|
658
|
+
}
|
|
668
659
|
// NOTE: to differentiate `callable` returning `void` (or
|
|
669
660
|
// explicitly `undefined`) from `stringify` returning an empty
|
|
670
661
|
// string we use `{ value: stringify(t) }`.
|
|
@@ -680,25 +671,20 @@ async function atLeastOrMostOnce(idempotencyAlias, context, callable, { stringif
|
|
|
680
671
|
// returning `void` (or explicitly `undefined`).
|
|
681
672
|
return "";
|
|
682
673
|
}, atMostOnce);
|
|
683
|
-
// NOTE: we parse and validate `value` every time
|
|
684
|
-
// time,
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// have to change the idempotency alias so that `callable` is
|
|
688
|
-
// re-executed. These semantics are the same as Python (although
|
|
689
|
-
// Python uses the `type` keyword argument instead of the
|
|
690
|
-
// `parse` and `validate` properties we use here).
|
|
674
|
+
// NOTE: we parse and validate `value` every time (even the first
|
|
675
|
+
// time, even though we validate above). These semantics are the
|
|
676
|
+
// same as Python (although Python uses the `type` keyword argument
|
|
677
|
+
// instead of the `parse` and `validate` properties we use here).
|
|
691
678
|
assert(result !== undefined);
|
|
692
679
|
if (result !== "") {
|
|
693
680
|
const { value } = JSON.parse(result);
|
|
694
681
|
const t = parse(value);
|
|
695
682
|
if (parse !== JSON.parse) {
|
|
696
683
|
if (validate === undefined) {
|
|
697
|
-
|
|
698
|
-
throw new Error("Missing `validate` property");
|
|
684
|
+
throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
|
|
699
685
|
}
|
|
700
686
|
else if (!validate(t)) {
|
|
701
|
-
throw new Error(
|
|
687
|
+
throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
|
|
702
688
|
}
|
|
703
689
|
}
|
|
704
690
|
return t;
|
|
@@ -708,7 +694,7 @@ async function atLeastOrMostOnce(idempotencyAlias, context, callable, { stringif
|
|
|
708
694
|
}
|
|
709
695
|
export async function atMostOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
|
|
710
696
|
try {
|
|
711
|
-
return await
|
|
697
|
+
return await memoize(idempotencyAlias, context, callable, {
|
|
712
698
|
...options,
|
|
713
699
|
atMostOnce: true,
|
|
714
700
|
});
|
|
@@ -721,7 +707,7 @@ export async function atMostOnce(idempotencyAlias, context, callable, options =
|
|
|
721
707
|
}
|
|
722
708
|
}
|
|
723
709
|
export async function atLeastOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
|
|
724
|
-
return
|
|
710
|
+
return memoize(idempotencyAlias, context, callable, {
|
|
725
711
|
...options,
|
|
726
712
|
atMostOnce: false,
|
|
727
713
|
});
|
|
@@ -733,7 +719,11 @@ export async function until(idempotencyAlias, context, callable, options = { val
|
|
|
733
719
|
const converge = () => {
|
|
734
720
|
return retry_reactively_until(context, callable);
|
|
735
721
|
};
|
|
736
|
-
return
|
|
722
|
+
return memoize(idempotencyAlias, context, converge, {
|
|
723
|
+
...options,
|
|
724
|
+
atMostOnce: false,
|
|
725
|
+
until: true,
|
|
726
|
+
});
|
|
737
727
|
}
|
|
738
728
|
const launchSubprocessConsensus = (base64_args) => {
|
|
739
729
|
// Create a child process via `fork` (which does not mean `fork` as
|
|
@@ -756,3 +746,43 @@ const callback = (...args) => {
|
|
|
756
746
|
};
|
|
757
747
|
ensurePythonVenv();
|
|
758
748
|
reboot_native.initialize(callback, Context.fromNativeExternal, launchSubprocessConsensus);
|
|
749
|
+
// TODO: move these into @reboot-dev/reboot-api and also generate them
|
|
750
|
+
// via plugin, perhaps via a new plugin which emits the Zod schemas
|
|
751
|
+
// for all protobuf messages, which might be easier after we've moved
|
|
752
|
+
// to protobuf-es v2 which has more natural TypeScript types.
|
|
753
|
+
function protobufToZodSchema(type) {
|
|
754
|
+
return z
|
|
755
|
+
.custom((t) => {
|
|
756
|
+
// TODO: replace this with `protobuf_es.isMessage` after upgrading
|
|
757
|
+
// '@bufbuild/protobuf'.
|
|
758
|
+
if (t instanceof type) {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
if (typeof t === "object" && "getType" in t) {
|
|
762
|
+
if (typeof t.getType === "function") {
|
|
763
|
+
const actualType = t.getType();
|
|
764
|
+
if (typeof actualType === "function" && "typeName" in actualType) {
|
|
765
|
+
return actualType.typeName === type.typeName;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
type.fromJson(t);
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
catch (e) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
.meta({ protobuf: type.typeName });
|
|
778
|
+
}
|
|
779
|
+
export const zod = {
|
|
780
|
+
tasks: {
|
|
781
|
+
TaskId: protobufToZodSchema(tasks_pb.TaskId),
|
|
782
|
+
},
|
|
783
|
+
bytes: z
|
|
784
|
+
.custom((array) => {
|
|
785
|
+
return array instanceof Uint8Array;
|
|
786
|
+
})
|
|
787
|
+
.meta({ protobuf: "bytes" }),
|
|
788
|
+
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"@bufbuild/protobuf": "1.3.2",
|
|
4
4
|
"@bufbuild/protoplugin": "1.3.2",
|
|
5
5
|
"@bufbuild/protoc-gen-es": "1.3.2",
|
|
6
|
-
"@reboot-dev/reboot-api": "0.
|
|
6
|
+
"@reboot-dev/reboot-api": "0.29.1",
|
|
7
7
|
"chalk": "^4.1.2",
|
|
8
8
|
"node-addon-api": "^7.0.0",
|
|
9
9
|
"node-gyp": ">=10.2.0",
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"extensionless": "^1.9.9",
|
|
13
13
|
"esbuild": "^0.24.0",
|
|
14
14
|
"express": "^5.1.0",
|
|
15
|
-
"@scarf/scarf": "1.4.0"
|
|
15
|
+
"@scarf/scarf": "1.4.0",
|
|
16
|
+
"tsx": "^4.19.2",
|
|
17
|
+
"zod": "^3.25.51"
|
|
16
18
|
},
|
|
17
19
|
"type": "module",
|
|
18
20
|
"name": "@reboot-dev/reboot",
|
|
19
|
-
"version": "0.
|
|
21
|
+
"version": "0.29.1",
|
|
20
22
|
"description": "npm package for Reboot",
|
|
21
23
|
"scripts": {
|
|
22
24
|
"preinstall": "node preinstall.cjs",
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
"prepack": "tsc"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"typescript": "
|
|
31
|
+
"typescript": "5.4.5",
|
|
30
32
|
"@types/express": "^5.0.1",
|
|
31
33
|
"@types/node": "20.11.5",
|
|
32
34
|
"@types/uuid": "^9.0.4",
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"bin": {
|
|
36
38
|
"rbt": "./rbt.js",
|
|
37
39
|
"rbt-esbuild": "./rbt-esbuild.js",
|
|
40
|
+
"rbt-schema-to-proto": "./rbt-schema-to-proto.js",
|
|
38
41
|
"protoc-gen-es": "./protoc-gen-es.js"
|
|
39
42
|
},
|
|
40
43
|
"engines": {
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
"rbt.d.ts",
|
|
55
58
|
"rbt-esbuild.d.ts",
|
|
56
59
|
"rbt-esbuild.js",
|
|
60
|
+
"rbt-schema-to-proto.d.ts",
|
|
61
|
+
"rbt-schema-to-proto.js",
|
|
57
62
|
"rbt.js",
|
|
58
63
|
"index.d.ts",
|
|
59
64
|
"index.js",
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { toPascalCase, toSnakeCase, ZOD_ERROR_NAMES, } from "@reboot-dev/reboot-api";
|
|
3
|
+
import { strict as assert } from "assert";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { mkdtemp } from "fs/promises";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
|
+
// Expected usage: rbt-schema-to-protoc filesDirectory path/relative/to/filesDirectory/to/file.ts ...
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const filesDirectory = args[0];
|
|
13
|
+
const files = args.slice(1);
|
|
14
|
+
const requestSchema = z.union([
|
|
15
|
+
z.instanceof(z.ZodObject),
|
|
16
|
+
z.record(z.string(), z.instanceof(z.ZodType)),
|
|
17
|
+
]);
|
|
18
|
+
const responseSchema = z.union([
|
|
19
|
+
z.instanceof(z.ZodObject),
|
|
20
|
+
z.record(z.string(), z.instanceof(z.ZodType)),
|
|
21
|
+
z.instanceof(z.ZodVoid),
|
|
22
|
+
]);
|
|
23
|
+
const errorsSchema = z.union([
|
|
24
|
+
z.tuple([z.instanceof(z.ZodObject)], z.instanceof(z.ZodObject)),
|
|
25
|
+
z.instanceof(z.ZodDiscriminatedUnion),
|
|
26
|
+
]);
|
|
27
|
+
const typeSchema = z.object({
|
|
28
|
+
state: z.union([
|
|
29
|
+
z.instanceof(z.ZodObject),
|
|
30
|
+
z.record(z.string(), z.instanceof(z.ZodType)),
|
|
31
|
+
]),
|
|
32
|
+
methods: z.record(z.string(), z.discriminatedUnion("kind", [
|
|
33
|
+
z.object({
|
|
34
|
+
kind: z.literal(["reader", "workflow"]),
|
|
35
|
+
request: requestSchema,
|
|
36
|
+
response: responseSchema,
|
|
37
|
+
errors: errorsSchema.optional(),
|
|
38
|
+
}),
|
|
39
|
+
z.object({
|
|
40
|
+
kind: z.literal(["writer", "transaction"]),
|
|
41
|
+
factory: z.object({}).optional(),
|
|
42
|
+
request: requestSchema,
|
|
43
|
+
response: responseSchema,
|
|
44
|
+
errors: errorsSchema.optional(),
|
|
45
|
+
}),
|
|
46
|
+
])),
|
|
47
|
+
});
|
|
48
|
+
// TODO:
|
|
49
|
+
//
|
|
50
|
+
// z.date (use well-known proto)
|
|
51
|
+
// z.enum (only support number enums)
|
|
52
|
+
// "recursive types"
|
|
53
|
+
// z.map (use protobuf map and only allow the mapped types that protobuf supports)
|
|
54
|
+
// z.set (use protobuf repeated but the fact that we must take a `Set` will ensure it's a set, and we could add annotations to do proto validations for other languages)
|
|
55
|
+
// z.tuple
|
|
56
|
+
//
|
|
57
|
+
// UNREPRESENTABLE:
|
|
58
|
+
//
|
|
59
|
+
// z.union is unlikely because if it is not "flattened" we won't know
|
|
60
|
+
// which fields to set when there are multiple possible "correct" fields
|
|
61
|
+
//
|
|
62
|
+
// z.intersection (suggest destructuring as that's what Zod suggests)
|
|
63
|
+
const Z_JSON_GETTER_TO_STRING = z.json()._zod.def.getter.toString();
|
|
64
|
+
const iszjson = (schema) => {
|
|
65
|
+
return (schema instanceof z.ZodLazy &&
|
|
66
|
+
schema._zod.def.getter?.toString() === Z_JSON_GETTER_TO_STRING);
|
|
67
|
+
};
|
|
68
|
+
const generate = (proto, { schema, path, name, state = false, }) => {
|
|
69
|
+
if (schema instanceof z.ZodObject) {
|
|
70
|
+
assert(name !== undefined);
|
|
71
|
+
proto.write(`message ${name} {`);
|
|
72
|
+
if (state) {
|
|
73
|
+
proto.write(`option (rbt.v1alpha1.state) = {};`);
|
|
74
|
+
}
|
|
75
|
+
const tags = new Map();
|
|
76
|
+
const shape = schema._zod.def.shape;
|
|
77
|
+
for (const key in shape) {
|
|
78
|
+
// TODO: ensure `key` is PascalCase.
|
|
79
|
+
let value = shape[key];
|
|
80
|
+
if (value === undefined || value._zod.def === undefined) {
|
|
81
|
+
// TODO: is this expected or possible or should we raise here
|
|
82
|
+
// instead?
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const meta = value.meta();
|
|
86
|
+
if (meta === undefined || !("tag" in meta)) {
|
|
87
|
+
console.error(chalk.stderr.bold.red(`Missing tag for property '${key}'; all properties of your object schemas must be tagged for backwards compatibility`));
|
|
88
|
+
process.exit(-1);
|
|
89
|
+
}
|
|
90
|
+
const { tag } = meta;
|
|
91
|
+
if (tags.has(tag)) {
|
|
92
|
+
// TODO: give "path" to this property.
|
|
93
|
+
console.error(chalk.stderr.bold.red(`Trying to use tag ${tag} with property '${key}' already used by '${tags.get(tag)}'`));
|
|
94
|
+
process.exit(-1);
|
|
95
|
+
}
|
|
96
|
+
tags.set(tag, key);
|
|
97
|
+
const field = toSnakeCase(key);
|
|
98
|
+
if (value instanceof z.ZodPipe) {
|
|
99
|
+
value = value.in;
|
|
100
|
+
}
|
|
101
|
+
else if (value instanceof z.ZodOptional) {
|
|
102
|
+
value = value._zod.def.innerType;
|
|
103
|
+
}
|
|
104
|
+
else if (value instanceof z.ZodDefault) {
|
|
105
|
+
const firstDefaultValue = value._zod.def.defaultValue;
|
|
106
|
+
const secondDefaultValue = value._zod.def.defaultValue;
|
|
107
|
+
switch (typeof firstDefaultValue) {
|
|
108
|
+
case "object":
|
|
109
|
+
if (firstDefaultValue === secondDefaultValue) {
|
|
110
|
+
console.error(chalk.stderr.bold.red(`'${path}.${key}' has a default value of type ` +
|
|
111
|
+
`${Array.isArray(firstDefaultValue) ? "array" : "object"} which ` +
|
|
112
|
+
`is dangerous and not supported, please use a ` +
|
|
113
|
+
`primitive type (e.g., string, number, boolean) ` +
|
|
114
|
+
`which is immutable or pass a function that creates ` +
|
|
115
|
+
`a new ${Array.isArray(firstDefaultValue) ? "array" : "object"} ` +
|
|
116
|
+
`each time\n`));
|
|
117
|
+
process.exit(-1);
|
|
118
|
+
}
|
|
119
|
+
// Otherwise, if objects are different, that means that
|
|
120
|
+
// the default value is a function that returns a new
|
|
121
|
+
// object each time, which is fine.
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
value = value._zod.def.innerType;
|
|
127
|
+
}
|
|
128
|
+
if (value._zod.def.type === "string") {
|
|
129
|
+
proto.write(`optional string ${field} = ${tag};`);
|
|
130
|
+
}
|
|
131
|
+
else if (value._zod.def.type === "number") {
|
|
132
|
+
proto.write(`optional double ${field} = ${tag};`);
|
|
133
|
+
}
|
|
134
|
+
else if (value._zod.def.type === "bigint") {
|
|
135
|
+
proto.write(`optional int64 ${field} = ${tag};`);
|
|
136
|
+
}
|
|
137
|
+
else if (value._zod.def.type === "boolean") {
|
|
138
|
+
proto.write(`optional bool ${field} = ${tag};`);
|
|
139
|
+
}
|
|
140
|
+
else if (value._zod.def.type === "literal") {
|
|
141
|
+
// Make the name of this nested type be the PascalCase property
|
|
142
|
+
// TODO: ensure `key` is already camelCase.
|
|
143
|
+
const typeName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
144
|
+
proto.write(`enum ${typeName} {`);
|
|
145
|
+
let i = 0;
|
|
146
|
+
for (const literal of value._zod.def.values) {
|
|
147
|
+
if (typeof literal !== "string") {
|
|
148
|
+
console.error(chalk.stderr.bold.red(`Unexpected literal '${literal}' for property '${key}'; only 'string' literals are currently supported`));
|
|
149
|
+
process.exit(-1);
|
|
150
|
+
}
|
|
151
|
+
proto.write(`${literal} = ${i++};`);
|
|
152
|
+
}
|
|
153
|
+
proto.write(`}`);
|
|
154
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
155
|
+
}
|
|
156
|
+
else if (value._zod.def.type === "array") {
|
|
157
|
+
// Make the name of this nested type be the PascalCase property
|
|
158
|
+
// TODO: ensure `key` is already camelCase.
|
|
159
|
+
const typeName = key.charAt(0).toUpperCase() + key.slice(1) + "Array";
|
|
160
|
+
proto.write(`message ${typeName} {`);
|
|
161
|
+
generate(proto, { schema: value, path: `${path}.${key}` });
|
|
162
|
+
proto.write(`}`);
|
|
163
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
164
|
+
}
|
|
165
|
+
else if (value._zod.def.type === "record") {
|
|
166
|
+
// Make the name of this nested type be the PascalCase property
|
|
167
|
+
// TODO: ensure `key` is already camelCase.
|
|
168
|
+
const typeName = key.charAt(0).toUpperCase() + key.slice(1) + "Record";
|
|
169
|
+
proto.write(`message ${typeName} {`);
|
|
170
|
+
generate(proto, { schema: value, path: `${path}.${key}` });
|
|
171
|
+
proto.write(`}`);
|
|
172
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
173
|
+
}
|
|
174
|
+
else if (value instanceof z.ZodDiscriminatedUnion) {
|
|
175
|
+
// `instanceof` b.c. type = "union".
|
|
176
|
+
// Make the name of this nested type be the PascalCase property
|
|
177
|
+
// TODO: ensure `key` is already camelCase.
|
|
178
|
+
const typeName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
179
|
+
generate(proto, {
|
|
180
|
+
schema: value,
|
|
181
|
+
path: `${path}.${key}`,
|
|
182
|
+
name: typeName,
|
|
183
|
+
});
|
|
184
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
185
|
+
}
|
|
186
|
+
else if (value._zod.def.type === "object") {
|
|
187
|
+
// Make the name of this nested type be the PascalCase property.
|
|
188
|
+
const typeName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
189
|
+
generate(proto, {
|
|
190
|
+
schema: value,
|
|
191
|
+
path: `${path}.${key}`,
|
|
192
|
+
name: typeName,
|
|
193
|
+
});
|
|
194
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
195
|
+
}
|
|
196
|
+
else if (value._zod.def.type === "custom" && "protobuf" in meta) {
|
|
197
|
+
const typeName = meta.protobuf;
|
|
198
|
+
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
199
|
+
}
|
|
200
|
+
else if (iszjson(value)) {
|
|
201
|
+
proto.write(`optional google.protobuf.Value ${field} = ${tag};`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.error(chalk.stderr.bold.red(`'${path}.${key}' has type '${value._zod.def.type}' which is not (yet) supported, please reach out to the maintainers!`));
|
|
205
|
+
process.exit(-1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
proto.write(`}\n`);
|
|
209
|
+
}
|
|
210
|
+
else if (schema instanceof z.ZodRecord) {
|
|
211
|
+
const keyType = schema.keyType;
|
|
212
|
+
// To account for all possible "string" types in Zod, e.g.,
|
|
213
|
+
// `z.uuid()`, `z.email()`, etc, we can't use `instanceof`.
|
|
214
|
+
if (keyType._zod.def.type !== "string") {
|
|
215
|
+
console.error(chalk.stderr.bold.red(`Unexpected record key type '${keyType._zod.def.type}' at '${path}'; only 'string' key types are currently supported for records`));
|
|
216
|
+
process.exit(-1);
|
|
217
|
+
}
|
|
218
|
+
const valueType = schema.valueType;
|
|
219
|
+
if (valueType instanceof z.ZodOptional) {
|
|
220
|
+
console.error(chalk.stderr.bold.red(`Record at '${path}' has _optional_ value type which is not supported`));
|
|
221
|
+
process.exit(-1);
|
|
222
|
+
}
|
|
223
|
+
else if (valueType instanceof z.ZodDefault) {
|
|
224
|
+
console.error(chalk.stderr.bold.red(`Record at '${path}' has _default_ value type which is not supported`));
|
|
225
|
+
process.exit(-1);
|
|
226
|
+
}
|
|
227
|
+
const typeName = (() => {
|
|
228
|
+
// To account for all possible "string" types in Zod, e.g.,
|
|
229
|
+
// `z.uuid()`, `z.email()`, etc, we can't use `instanceof`.
|
|
230
|
+
if (valueType._zod.def.type === "string") {
|
|
231
|
+
return "string";
|
|
232
|
+
}
|
|
233
|
+
else if (valueType instanceof z.ZodNumber) {
|
|
234
|
+
return "double";
|
|
235
|
+
}
|
|
236
|
+
else if (valueType instanceof z.ZodBigInt) {
|
|
237
|
+
return "int64";
|
|
238
|
+
}
|
|
239
|
+
else if (valueType instanceof z.ZodBoolean) {
|
|
240
|
+
return "bool";
|
|
241
|
+
}
|
|
242
|
+
else if (valueType instanceof z.ZodObject) {
|
|
243
|
+
return "Value";
|
|
244
|
+
}
|
|
245
|
+
else if (valueType instanceof z.ZodRecord) {
|
|
246
|
+
return "Value";
|
|
247
|
+
}
|
|
248
|
+
else if (valueType instanceof z.ZodArray) {
|
|
249
|
+
return "Value";
|
|
250
|
+
}
|
|
251
|
+
else if (valueType instanceof z.ZodDiscriminatedUnion) {
|
|
252
|
+
return "Value";
|
|
253
|
+
}
|
|
254
|
+
else if (valueType instanceof z.ZodCustom) {
|
|
255
|
+
const meta = valueType.meta();
|
|
256
|
+
if (valueType instanceof z.ZodCustom && "protobuf" in meta) {
|
|
257
|
+
return meta.protobuf;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (iszjson(valueType)) {
|
|
261
|
+
return "google.protobuf.Value";
|
|
262
|
+
}
|
|
263
|
+
console.error(chalk.stderr.bold.red(`Record at '${path}' has value type '${valueType._zod.def.type}' which is not (yet) supported`));
|
|
264
|
+
process.exit(-1);
|
|
265
|
+
})();
|
|
266
|
+
if (valueType instanceof z.ZodObject) {
|
|
267
|
+
generate(proto, {
|
|
268
|
+
schema: valueType,
|
|
269
|
+
path: `${path}.[value]`,
|
|
270
|
+
name: typeName,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else if (valueType instanceof z.ZodRecord) {
|
|
274
|
+
proto.write(`message ${typeName} {`);
|
|
275
|
+
generate(proto, {
|
|
276
|
+
schema: valueType,
|
|
277
|
+
path: `${path}.[value]`,
|
|
278
|
+
});
|
|
279
|
+
proto.write(`}`);
|
|
280
|
+
}
|
|
281
|
+
else if (valueType instanceof z.ZodArray) {
|
|
282
|
+
proto.write(`message ${typeName} {`);
|
|
283
|
+
generate(proto, {
|
|
284
|
+
schema: valueType,
|
|
285
|
+
path: `${path}.[value]`,
|
|
286
|
+
});
|
|
287
|
+
proto.write(`}`);
|
|
288
|
+
}
|
|
289
|
+
else if (valueType instanceof z.ZodDiscriminatedUnion) {
|
|
290
|
+
generate(proto, {
|
|
291
|
+
schema: valueType,
|
|
292
|
+
path: `${path}.[value]`,
|
|
293
|
+
name: typeName,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
proto.write(`map <string, ${typeName}> record = 1;`);
|
|
297
|
+
}
|
|
298
|
+
else if (schema instanceof z.ZodArray) {
|
|
299
|
+
const element = schema.element;
|
|
300
|
+
if (element instanceof z.ZodOptional) {
|
|
301
|
+
console.error(chalk.stderr.bold.red(`Array at '${path}' has _optional_ element type which is not supported`));
|
|
302
|
+
process.exit(-1);
|
|
303
|
+
}
|
|
304
|
+
else if (element instanceof z.ZodDefault) {
|
|
305
|
+
console.error(chalk.stderr.bold.red(`Array at '${path}' has _default_ element type which is not supported`));
|
|
306
|
+
process.exit(-1);
|
|
307
|
+
}
|
|
308
|
+
const typeName = (() => {
|
|
309
|
+
// To account for all possible "string" types in Zod, e.g.,
|
|
310
|
+
// `z.uuid()`, `z.email()`, etc, we can't use `instanceof`.
|
|
311
|
+
if (element._zod.def.type === "string") {
|
|
312
|
+
return "string";
|
|
313
|
+
}
|
|
314
|
+
else if (element instanceof z.ZodNumber) {
|
|
315
|
+
return "double";
|
|
316
|
+
}
|
|
317
|
+
else if (element instanceof z.ZodBigInt) {
|
|
318
|
+
return "int64";
|
|
319
|
+
}
|
|
320
|
+
else if (element instanceof z.ZodBoolean) {
|
|
321
|
+
return "bool";
|
|
322
|
+
}
|
|
323
|
+
else if (element instanceof z.ZodObject) {
|
|
324
|
+
return "Element";
|
|
325
|
+
}
|
|
326
|
+
else if (element instanceof z.ZodRecord) {
|
|
327
|
+
return "Element";
|
|
328
|
+
}
|
|
329
|
+
else if (element instanceof z.ZodArray) {
|
|
330
|
+
return "Element";
|
|
331
|
+
}
|
|
332
|
+
else if (element instanceof z.ZodDiscriminatedUnion) {
|
|
333
|
+
return "Element";
|
|
334
|
+
}
|
|
335
|
+
else if (element instanceof z.ZodCustom) {
|
|
336
|
+
const meta = element.meta();
|
|
337
|
+
if (element instanceof z.ZodCustom && "protobuf" in meta) {
|
|
338
|
+
return meta.protobuf;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (iszjson(element)) {
|
|
342
|
+
return "google.protobuf.Value";
|
|
343
|
+
}
|
|
344
|
+
console.error(chalk.stderr.bold.red(`Array at '${path}' has element type '${element._zod.def.type}' which is not (yet) supported`));
|
|
345
|
+
process.exit(-1);
|
|
346
|
+
})();
|
|
347
|
+
if (element instanceof z.ZodObject) {
|
|
348
|
+
generate(proto, {
|
|
349
|
+
schema: element,
|
|
350
|
+
path: `${path}.[element]`,
|
|
351
|
+
name: typeName,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else if (element instanceof z.ZodRecord) {
|
|
355
|
+
proto.write(`message ${typeName} {`);
|
|
356
|
+
generate(proto, {
|
|
357
|
+
schema: element,
|
|
358
|
+
path: `${path}.[element]`,
|
|
359
|
+
});
|
|
360
|
+
proto.write(`}`);
|
|
361
|
+
}
|
|
362
|
+
else if (element instanceof z.ZodArray) {
|
|
363
|
+
proto.write(`message ${typeName} {`);
|
|
364
|
+
generate(proto, {
|
|
365
|
+
schema: element,
|
|
366
|
+
path: `${path}.[element]`,
|
|
367
|
+
});
|
|
368
|
+
proto.write(`}`);
|
|
369
|
+
}
|
|
370
|
+
else if (element instanceof z.ZodDiscriminatedUnion) {
|
|
371
|
+
generate(proto, {
|
|
372
|
+
schema: element,
|
|
373
|
+
path: `${path}.[element]`,
|
|
374
|
+
name: typeName,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
proto.write(`repeated ${typeName} elements = 1;`);
|
|
378
|
+
}
|
|
379
|
+
else if (schema instanceof z.ZodDiscriminatedUnion) {
|
|
380
|
+
assert(name !== undefined);
|
|
381
|
+
proto.write(`message ${name} {`);
|
|
382
|
+
const discriminator = schema._zod.def.discriminator;
|
|
383
|
+
const literals = new Set();
|
|
384
|
+
const tags = new Map();
|
|
385
|
+
for (const option of schema.options) {
|
|
386
|
+
if (!(option instanceof z.ZodObject)) {
|
|
387
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has option of type '${option._zod.def.type}', should be 'object'`));
|
|
388
|
+
process.exit(-1);
|
|
389
|
+
}
|
|
390
|
+
if (!(discriminator in option.shape)) {
|
|
391
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has option missing discriminator '${discriminator}'`));
|
|
392
|
+
process.exit(-1);
|
|
393
|
+
}
|
|
394
|
+
if (!(option.shape[discriminator] instanceof z.ZodLiteral)) {
|
|
395
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has option with unexpected discriminator '${discriminator}'; only 'string' literals are currently supported`));
|
|
396
|
+
process.exit(-1);
|
|
397
|
+
}
|
|
398
|
+
for (const literal of option.shape[discriminator]._zod.def.values) {
|
|
399
|
+
if (typeof literal !== "string") {
|
|
400
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has option with unexpected discriminator '${discriminator}'; only 'string' literals are currently supported`));
|
|
401
|
+
process.exit(-1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Can only support one value because of how we name
|
|
405
|
+
// things. Could consider using the `tag` to name things instead
|
|
406
|
+
// so that the literal values could change, but it makes the
|
|
407
|
+
// proto less readable in places like the inspect page.
|
|
408
|
+
if (option.shape[discriminator]._zod.def.values.length !== 1) {
|
|
409
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has option with more than one discriminator '${discriminator}' literals, only one is currently supported`));
|
|
410
|
+
process.exit(-1);
|
|
411
|
+
}
|
|
412
|
+
const literal = option.shape[discriminator]._zod.def.values[0];
|
|
413
|
+
if (literals.has(literal)) {
|
|
414
|
+
console.error(chalk.stderr.bold.red(`Discriminated union at '${path}' has an option that is reusing the literal '${literal}' for discrimniator '${discriminator}'`));
|
|
415
|
+
process.exit(-1);
|
|
416
|
+
}
|
|
417
|
+
literals.add(literal);
|
|
418
|
+
// Make the name of this nested type be the PascalCase property.
|
|
419
|
+
const typeName = literal.charAt(0).toUpperCase() + literal.slice(1);
|
|
420
|
+
const meta = option.meta();
|
|
421
|
+
if (meta === undefined || !("tag" in meta)) {
|
|
422
|
+
console.error(chalk.stderr.bold.red(`Missing tag for discriminated union option at '${path}'; all discriminated union options must be tagged for backwards compatibility`));
|
|
423
|
+
process.exit(-1);
|
|
424
|
+
}
|
|
425
|
+
const { tag } = meta;
|
|
426
|
+
if (tags.has(tag)) {
|
|
427
|
+
console.error(chalk.stderr.bold.red(`Trying to use already used tag ${tag} in discriminated union`));
|
|
428
|
+
process.exit(-1);
|
|
429
|
+
}
|
|
430
|
+
tags.set(tag, [toSnakeCase(literal), typeName]);
|
|
431
|
+
const omit = {};
|
|
432
|
+
omit[discriminator] = true;
|
|
433
|
+
generate(proto, {
|
|
434
|
+
schema: option.omit(omit),
|
|
435
|
+
path: `${path}.{ ${discriminator}: "${literal}", ... }`,
|
|
436
|
+
name: typeName,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
proto.write(`oneof ${discriminator} {`);
|
|
440
|
+
for (const [tag, [literal, typeName]] of tags) {
|
|
441
|
+
proto.write(`${typeName} ${literal} = ${tag};`);
|
|
442
|
+
}
|
|
443
|
+
proto.write(`}`);
|
|
444
|
+
proto.write(`}`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
throw new Error(`Unexpected type '${schema._zod.def.type}'`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const main = async () => {
|
|
451
|
+
// We generate the `.proto` files in a temporary directory.
|
|
452
|
+
const generatedProtosDirectory = await mkdtemp(path.join(os.tmpdir(), "protos-"));
|
|
453
|
+
const cwd = process.cwd();
|
|
454
|
+
let protoFileGenerated = false;
|
|
455
|
+
for (const file of files) {
|
|
456
|
+
const module = await import(`${path.join(cwd, filesDirectory, file)}`);
|
|
457
|
+
const api = module.api;
|
|
458
|
+
if (api === undefined) {
|
|
459
|
+
// Skipping the file if it does not export `api`.
|
|
460
|
+
// We will error out from 'protoc' if none of the files
|
|
461
|
+
// exported `api`.
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
// Get '.proto' file name, i.e., by changing '.ts' to '.proto'.
|
|
465
|
+
//
|
|
466
|
+
// NOTE: there is an prerequisite invariant here that the paths
|
|
467
|
+
// are already in the correct form, hence why we have to join with
|
|
468
|
+
// all of `cwd`, `filesDirectory`, and `file` above.
|
|
469
|
+
const parsed = path.parse(file);
|
|
470
|
+
parsed.ext = ".proto";
|
|
471
|
+
parsed.base = parsed.name + parsed.ext;
|
|
472
|
+
const name = `${path.format(parsed)}`;
|
|
473
|
+
fs.mkdirSync(path.dirname(path.join(generatedProtosDirectory, name)), {
|
|
474
|
+
recursive: true,
|
|
475
|
+
});
|
|
476
|
+
const proto = fs.createWriteStream(path.join(generatedProtosDirectory, name));
|
|
477
|
+
proto.write(`syntax = "proto3";\n`);
|
|
478
|
+
proto.write(`package ${parsed.dir.replace(/\//g, ".")};\n`);
|
|
479
|
+
proto.write(`import "google/protobuf/empty.proto";\n`);
|
|
480
|
+
proto.write(`import "google/protobuf/struct.proto";\n`);
|
|
481
|
+
proto.write(`import "rbt/v1alpha1/options.proto";\n`);
|
|
482
|
+
// Including 'rbt/v1alpha1/tasks.proto' preemptively to support
|
|
483
|
+
// protobuf messages like `TaskId`.
|
|
484
|
+
proto.write(`import "rbt/v1alpha1/tasks.proto";\n`);
|
|
485
|
+
proto.write(`option (rbt.v1alpha1.file).zod = "${path.join(cwd, filesDirectory, file)}";\n`);
|
|
486
|
+
for (const typeName in api) {
|
|
487
|
+
// TODO: ensure `typeName` is PascalCase.
|
|
488
|
+
const result = typeSchema.safeParse(api[typeName]);
|
|
489
|
+
if (!result.success) {
|
|
490
|
+
console.error(chalk.stderr.bold.red(`'api.${typeName}' in '${path.join(filesDirectory, file)}' could not be validated! Please try again after correcting the following errors: \n\n${z.prettifyError(result.error)}\n`));
|
|
491
|
+
// Check if we have any 'not instance of ZodType' errors,
|
|
492
|
+
// possibly indicating that 'zod/v4' is not being
|
|
493
|
+
// used. Unfortunately Zod does not provide a way to check
|
|
494
|
+
// what version of the library is being used, so this is just
|
|
495
|
+
// best effort.
|
|
496
|
+
for (const issue of result.error.issues) {
|
|
497
|
+
if (JSON.stringify(issue).includes("not instance of ZodType")) {
|
|
498
|
+
console.error(chalk.stderr.bold.red(`NOTE: Reboot requires 'zod/v4', are you using it?\n`));
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
process.exit(-1);
|
|
503
|
+
}
|
|
504
|
+
const type = result.data;
|
|
505
|
+
generate(proto, {
|
|
506
|
+
schema: type.state instanceof z.ZodObject ? type.state : z.object(type.state),
|
|
507
|
+
path: `api.${typeName}.state`,
|
|
508
|
+
name: typeName,
|
|
509
|
+
state: true,
|
|
510
|
+
});
|
|
511
|
+
const errorsToGenerate = [];
|
|
512
|
+
for (const methodName in type.methods) {
|
|
513
|
+
// TODO: ensure `methodName` is PascalCase.
|
|
514
|
+
const { request, response } = type.methods[methodName];
|
|
515
|
+
const requestTypeName = `${typeName}${toPascalCase(methodName)}Request`;
|
|
516
|
+
generate(proto, {
|
|
517
|
+
schema: request instanceof z.ZodObject ? request : z.object(request),
|
|
518
|
+
path: `api.${typeName}.methods.${methodName}.request`,
|
|
519
|
+
name: requestTypeName,
|
|
520
|
+
});
|
|
521
|
+
if (response instanceof z.ZodVoid) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const responseTypeName = `${typeName}${toPascalCase(methodName)}Response`;
|
|
525
|
+
generate(proto, {
|
|
526
|
+
schema: response instanceof z.ZodObject ? response : z.object(response),
|
|
527
|
+
path: `api.${typeName}.methods.${methodName}.response`,
|
|
528
|
+
name: responseTypeName,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
proto.write(`service ${typeName}Methods {\n`);
|
|
532
|
+
for (const methodName in type.methods) {
|
|
533
|
+
// TODO: ensure `methodName` is PascalCase.
|
|
534
|
+
const method = type.methods[methodName];
|
|
535
|
+
const requestTypeName = `${typeName}${toPascalCase(methodName)}Request`;
|
|
536
|
+
const responseTypeName = method.response instanceof z.ZodVoid
|
|
537
|
+
? `google.protobuf.Empty`
|
|
538
|
+
: `${typeName}${toPascalCase(methodName)}Response`;
|
|
539
|
+
proto.write([
|
|
540
|
+
` rpc ${toPascalCase(methodName)}(${requestTypeName})`,
|
|
541
|
+
` returns (${responseTypeName}) {`,
|
|
542
|
+
` option (rbt.v1alpha1.method) = {`,
|
|
543
|
+
` ${method.kind}: {`,
|
|
544
|
+
].join(`\n`));
|
|
545
|
+
if (method.kind === "writer" || method.kind === "transaction") {
|
|
546
|
+
if (method.factory !== undefined) {
|
|
547
|
+
proto.write(` constructor: {},\n`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
proto.write(` },\n`);
|
|
551
|
+
if (method.errors !== undefined) {
|
|
552
|
+
const errors = method.errors instanceof z.ZodDiscriminatedUnion
|
|
553
|
+
? method.errors
|
|
554
|
+
: z.discriminatedUnion("type", method.errors);
|
|
555
|
+
const path = `api.${typeName}.methods.${methodName}.errors`;
|
|
556
|
+
const name = `${typeName}${toPascalCase(methodName)}Errors`;
|
|
557
|
+
errorsToGenerate.push(() => {
|
|
558
|
+
generate(proto, { schema: errors, path, name });
|
|
559
|
+
});
|
|
560
|
+
// In addition to all the checks we get above when calling
|
|
561
|
+
// `generate()`, we also want to make sure that none of the
|
|
562
|
+
// gRPC or Reboot error types conflict.
|
|
563
|
+
for (const option of errors.options) {
|
|
564
|
+
const literal = option.shape.type._zod.def.values[0];
|
|
565
|
+
if (ZOD_ERROR_NAMES.includes(literal)) {
|
|
566
|
+
console.error(chalk.stderr.bold.red(`'${path}' uses '${literal}' as an error 'type' that conflicts with system errors, please use a different literal!\n`));
|
|
567
|
+
process.exit(-1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// And finally, we need to add this protobuf message to the
|
|
571
|
+
// `errors` option for this method in the `.proto` file.
|
|
572
|
+
proto.write(` errors: ["${name}"],\n`);
|
|
573
|
+
}
|
|
574
|
+
proto.write([` };`, ` }`].join(`\n`));
|
|
575
|
+
}
|
|
576
|
+
proto.write(`}\n`);
|
|
577
|
+
// Generate all the errors we need at the top-level.
|
|
578
|
+
errorsToGenerate.forEach((generate) => generate());
|
|
579
|
+
}
|
|
580
|
+
proto.end();
|
|
581
|
+
// Need to wait for the file to be written (TODO: wait for all of
|
|
582
|
+
// them via `Promise.all`).
|
|
583
|
+
await new Promise((resolve, reject) => {
|
|
584
|
+
proto.on("finish", () => resolve());
|
|
585
|
+
proto.on("error", (err) => reject(err));
|
|
586
|
+
});
|
|
587
|
+
protoFileGenerated = true;
|
|
588
|
+
}
|
|
589
|
+
if (protoFileGenerated) {
|
|
590
|
+
// If at least one proto file was generated, we write the
|
|
591
|
+
// directory to the stdout, so Python 'generate' script can
|
|
592
|
+
// glob inside it and adjust the 'protoc' generation command.
|
|
593
|
+
console.log(`${generatedProtosDirectory}`);
|
|
594
|
+
}
|
|
595
|
+
process.exit(0);
|
|
596
|
+
};
|
|
597
|
+
main();
|
package/reboot_native.cc
CHANGED
|
@@ -2597,7 +2597,7 @@ Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
|
|
|
2597
2597
|
}
|
|
2598
2598
|
|
|
2599
2599
|
|
|
2600
|
-
Napi::Value
|
|
2600
|
+
Napi::Value memoize(const Napi::CallbackInfo& info) {
|
|
2601
2601
|
auto js_external_context = NapiSafeReference(
|
|
2602
2602
|
info[0].As<Napi::External<py::object>>());
|
|
2603
2603
|
|
|
@@ -2840,8 +2840,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
2840
2840
|
Napi::Function::New<retry_reactively_until>(env));
|
|
2841
2841
|
|
|
2842
2842
|
exports.Set(
|
|
2843
|
-
Napi::String::New(env, "
|
|
2844
|
-
Napi::Function::New<
|
|
2843
|
+
Napi::String::New(env, "memoize"),
|
|
2844
|
+
Napi::Function::New<memoize>(env));
|
|
2845
2845
|
|
|
2846
2846
|
exports.Set(
|
|
2847
2847
|
Napi::String::New(env, "Task_await"),
|
package/reboot_native.cjs
CHANGED
|
@@ -85,7 +85,7 @@ exports.Context_generateIdempotentStateId =
|
|
|
85
85
|
reboot_native.exports.Context_generateIdempotentStateId;
|
|
86
86
|
exports.WriterContext_set_sync = reboot_native.exports.WriterContext_set_sync;
|
|
87
87
|
exports.retry_reactively_until = reboot_native.exports.retry_reactively_until;
|
|
88
|
-
exports.
|
|
88
|
+
exports.memoize = reboot_native.exports.memoize;
|
|
89
89
|
exports.Servicer_read = reboot_native.exports.Servicer_read;
|
|
90
90
|
exports.Servicer_write = reboot_native.exports.Servicer_write;
|
|
91
91
|
exports.importPy = reboot_native.exports.importPy;
|
package/reboot_native.d.ts
CHANGED
package/secrets/index.d.ts
CHANGED
package/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const REBOOT_VERSION = "0.
|
|
1
|
+
export declare const REBOOT_VERSION = "0.29.1";
|
package/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const REBOOT_VERSION = "0.
|
|
1
|
+
export const REBOOT_VERSION = "0.29.1";
|