@volter/twin-standard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +20 -0
- package/dist/src/check-sources.d.ts +33 -0
- package/dist/src/check-sources.js +128 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +105 -0
- package/dist/src/derive.d.ts +2 -0
- package/dist/src/derive.js +349 -0
- package/dist/src/gate.d.ts +9 -0
- package/dist/src/gate.js +109 -0
- package/dist/src/grade.d.ts +30 -0
- package/dist/src/grade.js +36 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +11 -0
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +33 -0
- package/dist/src/p3-rules.d.ts +8 -0
- package/dist/src/p3-rules.js +43 -0
- package/dist/src/protocol-3.d.ts +5 -0
- package/dist/src/protocol-3.js +721 -0
- package/dist/src/published.d.ts +13 -0
- package/dist/src/published.js +36 -0
- package/dist/src/spec-documents.d.ts +12 -0
- package/dist/src/spec-documents.js +61 -0
- package/dist/src/spec-ir-client.d.ts +20 -0
- package/dist/src/spec-ir-client.js +77 -0
- package/dist/src/spec-ir-commands.d.ts +38 -0
- package/dist/src/spec-ir-commands.js +38 -0
- package/dist/src/spec-ir-discovery.d.ts +11 -0
- package/dist/src/spec-ir-discovery.js +106 -0
- package/dist/src/spec-ir-graphql.d.ts +25 -0
- package/dist/src/spec-ir-graphql.js +46 -0
- package/dist/src/spec-ir-lines.d.ts +19 -0
- package/dist/src/spec-ir-lines.js +76 -0
- package/dist/src/spec-ir-proto.d.ts +80 -0
- package/dist/src/spec-ir-proto.js +339 -0
- package/dist/src/spec-ir.d.ts +104 -0
- package/dist/src/spec-ir.js +691 -0
- package/dist/src/spec-patches.d.ts +8 -0
- package/dist/src/spec-patches.js +24 -0
- package/dist/src/spec.d.ts +5 -0
- package/dist/src/spec.js +7 -0
- package/dist/src/types.d.ts +75 -0
- package/dist/src/types.js +4 -0
- package/dist/src/unit.d.ts +5 -0
- package/dist/src/unit.js +14 -0
- package/package.json +71 -0
- package/src/check-sources.ts +109 -0
- package/src/cli.ts +75 -0
- package/src/derive.ts +316 -0
- package/src/gate.ts +95 -0
- package/src/grade.ts +47 -0
- package/src/index.ts +12 -0
- package/src/lanes.ts +29 -0
- package/src/p3-rules.ts +44 -0
- package/src/protocol-3.ts +617 -0
- package/src/published.ts +37 -0
- package/src/spec-documents.ts +58 -0
- package/src/spec-ir-client.ts +86 -0
- package/src/spec-ir-commands.ts +50 -0
- package/src/spec-ir-discovery.ts +104 -0
- package/src/spec-ir-graphql.ts +64 -0
- package/src/spec-ir-lines.ts +76 -0
- package/src/spec-ir-proto.ts +289 -0
- package/src/spec-ir.ts +689 -0
- package/src/spec-patches.ts +23 -0
- package/src/spec.ts +8 -0
- package/src/types.ts +53 -0
- package/src/unit.ts +15 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// A Protocol Buffers service as the IR (spec-ir.ts): the spec of a vendor whose API is RPCs over HTTP rather than
|
|
2
|
+
// REST paths (PlanetScale's psdb data plane, `psdb.v1alpha1.Database`, which @planetscale/database calls). The
|
|
3
|
+
// `.proto` files are the vendor's published words, vendored under `spec/proto/` with every file they import.
|
|
4
|
+
//
|
|
5
|
+
// Connect's unary protocol (connectrpc.com/docs/protocol) addresses an rpc as `POST /<package>.<Service>/<Method>`
|
|
6
|
+
// with the request message as its body in the proto3 JSON mapping (protobuf.dev/programming-guides/json): a field is
|
|
7
|
+
// named by its lowerCamelCase JSON name, 64-bit integers are strings, bytes are base64, enums are their value names,
|
|
8
|
+
// a map is an object, and a field holding its default value may be omitted. So each rpc is one operation: its request
|
|
9
|
+
// message's fields are the body, and its response message is what it answers. A service's operations act on no
|
|
10
|
+
// resource path, so each is an `action`; the response messages are the resources, as a spec that declares none yields
|
|
11
|
+
// resources named after its response schemas.
|
|
12
|
+
|
|
13
|
+
import type { IrOperation, IrParam, IrResource, SpecIR } from './spec-ir.ts';
|
|
14
|
+
|
|
15
|
+
type ProtoField = { name: string; jsonName: string; number: number; type: string; repeated: boolean; map?: { key: string; value: string }; oneof?: string };
|
|
16
|
+
type ProtoMessage = { fullName: string; fields: ProtoField[] };
|
|
17
|
+
type ProtoEnum = { fullName: string; values: string[]; numbers: Record<string, number> };
|
|
18
|
+
type ProtoRpc = { name: string; request: string; response: string; clientStreams: boolean; serverStreams: boolean };
|
|
19
|
+
type ProtoService = { fullName: string; rpcs: ProtoRpc[] };
|
|
20
|
+
export type ProtoFile = { path: string; pkg: string; messages: ProtoMessage[]; enums: ProtoEnum[]; services: ProtoService[] };
|
|
21
|
+
|
|
22
|
+
const SCALARS: Record<string, string> = {
|
|
23
|
+
double: 'number', float: 'number',
|
|
24
|
+
int32: 'integer', uint32: 'integer', sint32: 'integer', fixed32: 'integer', sfixed32: 'integer',
|
|
25
|
+
// proto3 JSON writes 64-bit integers as decimal strings
|
|
26
|
+
int64: 'string', uint64: 'string', sint64: 'string', fixed64: 'string', sfixed64: 'string',
|
|
27
|
+
bool: 'boolean', string: 'string', bytes: 'string',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** protoc's JSON name: underscores dropped, the letter after each upper-cased. */
|
|
31
|
+
const jsonNameOf = (name: string): string => name.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
32
|
+
|
|
33
|
+
function tokens(text: string): string[] {
|
|
34
|
+
// strings, comments and tokens in one pass: a `//` inside a string ("https://…") is the string's, not a comment
|
|
35
|
+
const all = text.match(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*|\/\*[\s\S]*?\*\/|[A-Za-z_][\w.]*|0[xX][0-9a-fA-F]+|\d+(?:\.\d+)?|[{}()<>;=,[\]-]/g) ?? [];
|
|
36
|
+
return all.filter((x) => !x.startsWith('//') && !x.startsWith('/*'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One `.proto` file's declarations. */
|
|
40
|
+
export function parseProto(path: string, text: string): ProtoFile {
|
|
41
|
+
const t = tokens(text);
|
|
42
|
+
let i = 0;
|
|
43
|
+
const file: ProtoFile = { path, pkg: '', messages: [], enums: [], services: [] };
|
|
44
|
+
const expect = (tok: string): void => { if (t[i] !== tok) throw new Error(`spec-ir-proto: ${path}: expected "${tok}", found "${t[i]}"`); i++; };
|
|
45
|
+
// a statement ends at its `;`, or at the `}` closing a block that is its body (`extend X { … }`); braces inside
|
|
46
|
+
// brackets or an option's value (`[declaration = { … }]`, `option (x) = { … };`) do not end it
|
|
47
|
+
const skipStatement = (): void => {
|
|
48
|
+
let depth = 0;
|
|
49
|
+
while (i < t.length) {
|
|
50
|
+
const x = t[i++];
|
|
51
|
+
if (x === '{' || x === '[') depth++;
|
|
52
|
+
else if (x === '}' || x === ']') { depth--; if (depth <= 0 && x === '}' && t[i] !== ';') return; }
|
|
53
|
+
else if (x === ';' && depth <= 0) return;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// a field's options, nested brackets and all (`[(google.api.field_behavior) = REQUIRED, (x) = { a: [1] }]`)
|
|
57
|
+
const skipOptions = (): void => { if (t[i] !== '[') return; let depth = 0; do { if (t[i] === '[') depth++; else if (t[i] === ']') depth--; i++; } while (depth > 0 && i < t.length); };
|
|
58
|
+
const scope = (): string => file.pkg;
|
|
59
|
+
const parseEnum = (prefix: string): void => {
|
|
60
|
+
const name = t[i++]!;
|
|
61
|
+
expect('{');
|
|
62
|
+
const values: string[] = [];
|
|
63
|
+
const numbers: Record<string, number> = {};
|
|
64
|
+
while (t[i] !== '}') {
|
|
65
|
+
if (t[i] === 'option' || t[i] === 'reserved') { skipStatement(); continue; }
|
|
66
|
+
const v = t[i++]!;
|
|
67
|
+
expect('=');
|
|
68
|
+
// a negative number is two tokens
|
|
69
|
+
const negative = t[i] === '-' ? (i++, true) : false;
|
|
70
|
+
numbers[v] = (negative ? -1 : 1) * Number(t[i++]);
|
|
71
|
+
skipOptions();
|
|
72
|
+
expect(';');
|
|
73
|
+
values.push(v);
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
file.enums.push({ fullName: `${prefix}.${name}`, values, numbers });
|
|
77
|
+
};
|
|
78
|
+
const parseField = (fields: ProtoField[], oneof?: string): void => {
|
|
79
|
+
let repeated = false;
|
|
80
|
+
if (t[i] === 'repeated') { repeated = true; i++; } else if (t[i] === 'optional' || t[i] === 'required') i++;
|
|
81
|
+
let type: string;
|
|
82
|
+
let map: ProtoField['map'];
|
|
83
|
+
if (t[i] === 'map') {
|
|
84
|
+
i++;
|
|
85
|
+
expect('<');
|
|
86
|
+
const key = t[i++]!;
|
|
87
|
+
expect(',');
|
|
88
|
+
const value = t[i++]!;
|
|
89
|
+
expect('>');
|
|
90
|
+
map = { key, value };
|
|
91
|
+
type = value;
|
|
92
|
+
} else type = t[i++]!;
|
|
93
|
+
const name = t[i++]!;
|
|
94
|
+
expect('=');
|
|
95
|
+
const number = Number(t[i++]);
|
|
96
|
+
let jsonName = jsonNameOf(name);
|
|
97
|
+
if (t[i] === '[') {
|
|
98
|
+
// a declared json_name overrides the derived one
|
|
99
|
+
const end = t.indexOf(']', i);
|
|
100
|
+
const at = t.slice(i, end).indexOf('json_name');
|
|
101
|
+
if (at >= 0) jsonName = JSON.parse(t[i + at + 2]!);
|
|
102
|
+
i = end + 1;
|
|
103
|
+
}
|
|
104
|
+
expect(';');
|
|
105
|
+
fields.push({ name, jsonName, number, type, repeated, ...(map ? { map } : {}), ...(oneof ? { oneof } : {}) });
|
|
106
|
+
};
|
|
107
|
+
const parseMessage = (prefix: string): void => {
|
|
108
|
+
const name = t[i++]!;
|
|
109
|
+
const fullName = `${prefix}.${name}`;
|
|
110
|
+
expect('{');
|
|
111
|
+
const fields: ProtoField[] = [];
|
|
112
|
+
while (t[i] !== '}') {
|
|
113
|
+
const x = t[i];
|
|
114
|
+
if (x === 'message') { i++; parseMessage(fullName); }
|
|
115
|
+
else if (x === 'enum') { i++; parseEnum(fullName); }
|
|
116
|
+
else if (x === 'oneof') {
|
|
117
|
+
i++;
|
|
118
|
+
const group = t[i++]!;
|
|
119
|
+
expect('{');
|
|
120
|
+
while (t[i] !== '}') { if (t[i] === 'option') { skipStatement(); continue; } parseField(fields, group); }
|
|
121
|
+
i++;
|
|
122
|
+
} else if (x === 'option' || x === 'reserved' || x === 'extensions' || x === 'extend') skipStatement();
|
|
123
|
+
else if (x === ';') i++;
|
|
124
|
+
else parseField(fields);
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
file.messages.push({ fullName, fields });
|
|
128
|
+
};
|
|
129
|
+
while (i < t.length) {
|
|
130
|
+
const x = t[i++];
|
|
131
|
+
if (x === 'package') { file.pkg = t[i++]!; expect(';'); }
|
|
132
|
+
else if (x === 'message') parseMessage(scope());
|
|
133
|
+
else if (x === 'enum') parseEnum(scope());
|
|
134
|
+
else if (x === 'service') {
|
|
135
|
+
const name = t[i++]!;
|
|
136
|
+
expect('{');
|
|
137
|
+
const rpcs: ProtoRpc[] = [];
|
|
138
|
+
while (t[i] !== '}') {
|
|
139
|
+
if (t[i] !== 'rpc') { skipStatement(); continue; }
|
|
140
|
+
i++;
|
|
141
|
+
const rpc = t[i++]!;
|
|
142
|
+
expect('(');
|
|
143
|
+
const clientStreams = t[i] === 'stream' ? (i++, true) : false;
|
|
144
|
+
const request = t[i++]!;
|
|
145
|
+
expect(')');
|
|
146
|
+
expect('returns');
|
|
147
|
+
expect('(');
|
|
148
|
+
const serverStreams = t[i] === 'stream' ? (i++, true) : false;
|
|
149
|
+
const response = t[i++]!;
|
|
150
|
+
expect(')');
|
|
151
|
+
if (t[i] === '{') { let depth = 0; do { if (t[i] === '{') depth++; else if (t[i] === '}') depth--; i++; } while (depth > 0); }
|
|
152
|
+
else expect(';');
|
|
153
|
+
rpcs.push({ name: rpc, request, response, clientStreams, serverStreams });
|
|
154
|
+
}
|
|
155
|
+
i++;
|
|
156
|
+
file.services.push({ fullName: `${file.pkg}.${name}`, rpcs });
|
|
157
|
+
} else if (x === 'syntax' || x === 'import' || x === 'option' || x === 'edition' || x === 'extend') skipStatement(); // an `extend` adds options, never a wire type
|
|
158
|
+
else if (x === ';') continue;
|
|
159
|
+
else throw new Error(`spec-ir-proto: ${path}: unexpected "${x}"`);
|
|
160
|
+
}
|
|
161
|
+
return file;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The vendored tree (path → text) as data, so a spec patch corrects it as it corrects a JSON spec (derive-pack). */
|
|
165
|
+
export type ProtoSpec = { format: 'proto'; files: ProtoFile[] };
|
|
166
|
+
export function readProtoFiles(files: Record<string, string>): ProtoSpec {
|
|
167
|
+
return { format: 'proto', files: Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([path, text]) => parseProto(path, text)) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The IR of the services the vendored files declare: every rpc of every service one operation. */
|
|
171
|
+
export function fromProto(spec: ProtoSpec): SpecIR {
|
|
172
|
+
const parsed = spec.files;
|
|
173
|
+
const messages = new Map(parsed.flatMap((f) => f.messages.map((m) => [m.fullName, m] as const)));
|
|
174
|
+
const enums = new Map(parsed.flatMap((f) => f.enums.map((e) => [e.fullName, e] as const)));
|
|
175
|
+
// a type name as written resolves from the innermost scope outward (protobuf's scoping rule)
|
|
176
|
+
const resolve = (name: string, scopeName: string): string | undefined => {
|
|
177
|
+
if (name.startsWith('.')) return name.slice(1);
|
|
178
|
+
const parts = scopeName.split('.');
|
|
179
|
+
for (let n = parts.length; n >= 0; n--) {
|
|
180
|
+
const candidate = [...parts.slice(0, n), name].filter(Boolean).join('.');
|
|
181
|
+
if (messages.has(candidate) || enums.has(candidate)) return candidate;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
};
|
|
185
|
+
const short = (fullName: string): string => fullName.split('.').pop()!;
|
|
186
|
+
const typeOf = (f: ProtoField, scopeName: string): { type: string; enum?: string[] } => {
|
|
187
|
+
if (f.map) return { type: 'object' };
|
|
188
|
+
if (f.repeated) return { type: 'array' };
|
|
189
|
+
if (SCALARS[f.type]) return { type: SCALARS[f.type]! };
|
|
190
|
+
const full = resolve(f.type, scopeName);
|
|
191
|
+
if (full && enums.has(full)) return { type: 'string', enum: enums.get(full)!.values };
|
|
192
|
+
if (!full) throw new Error(`spec-ir-proto: ${scopeName}: type ${f.type} is not declared in the vendored files`);
|
|
193
|
+
return { type: short(full) };
|
|
194
|
+
};
|
|
195
|
+
const services = parsed.flatMap((f) => f.services);
|
|
196
|
+
if (!services.length) throw new Error('spec-ir-proto: the vendored files declare no service');
|
|
197
|
+
const operations: IrOperation[] = [];
|
|
198
|
+
const answered = new Set<string>();
|
|
199
|
+
for (const svc of services) {
|
|
200
|
+
const pkg = svc.fullName.split('.').slice(0, -1).join('.');
|
|
201
|
+
for (const rpc of svc.rpcs) {
|
|
202
|
+
const request = resolve(rpc.request, pkg);
|
|
203
|
+
const response = resolve(rpc.response, pkg);
|
|
204
|
+
if (!request || !response) throw new Error(`spec-ir-proto: ${svc.fullName}.${rpc.name}: an argument type is not declared`);
|
|
205
|
+
answered.add(response);
|
|
206
|
+
const body: IrParam[] = messages.get(request)!.fields
|
|
207
|
+
.map((f) => ({ name: f.jsonName, type: typeOf(f, request).type, required: false }))
|
|
208
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
209
|
+
operations.push({
|
|
210
|
+
id: rpc.name,
|
|
211
|
+
method: 'post',
|
|
212
|
+
path: `/${svc.fullName}/${rpc.name}`,
|
|
213
|
+
pathParams: [],
|
|
214
|
+
query: [],
|
|
215
|
+
body,
|
|
216
|
+
bodyEncoding: 'json',
|
|
217
|
+
successStatus: 200,
|
|
218
|
+
...(rpc.serverStreams ? { streams: true } : {}),
|
|
219
|
+
answers: { resource: short(response), list: false },
|
|
220
|
+
class: 'action',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// proto3 JSON may omit any field holding its default, so none is required; a message field may be null
|
|
225
|
+
const resources: IrResource[] = [...answered].sort().map((full) => {
|
|
226
|
+
const m = messages.get(full)!;
|
|
227
|
+
const fields = m.fields.map((f) => {
|
|
228
|
+
const t = typeOf(f, full);
|
|
229
|
+
return { name: f.jsonName, type: t.type, nullable: !SCALARS[f.type] && !t.enum && !f.repeated && !f.map, ...(t.enum ? { enum: t.enum } : {}) };
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
name: short(full),
|
|
233
|
+
schema: short(full),
|
|
234
|
+
fields,
|
|
235
|
+
stateCandidates: fields.filter((f) => f.enum && f.enum.length > 1).map((f) => ({ field: f.name, values: f.enum! })),
|
|
236
|
+
expandable: [],
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
return { format: 'proto', version: services.map((s) => s.fullName).join(', '), resources, operations };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A field as the kernel's protobuf codec reads it: its number, its JSON name, and its type resolved (a scalar's name, or
|
|
243
|
+
* a message's or an enum's full name with its kind). */
|
|
244
|
+
export type ProtoSchemaField = { name: string; json: string; number: number; type: string; kind: 'scalar' | 'message' | 'enum'; repeated?: true; map?: { key: string; value: string; valueKind: 'scalar' | 'message' | 'enum' } };
|
|
245
|
+
/** The schema the derive writes for a proto unit (`src/generated/proto.gen.json`): every message's fields, every enum's
|
|
246
|
+
* numbers, and each rpc's path, request and response, all by full name. */
|
|
247
|
+
export type ProtoSchema = {
|
|
248
|
+
messages: Record<string, ProtoSchemaField[]>;
|
|
249
|
+
enums: Record<string, Record<string, number>>;
|
|
250
|
+
rpcs: Record<string, { operation: string; request: string; response: string; serverStreams?: true; clientStreams?: true }>;
|
|
251
|
+
};
|
|
252
|
+
export function protoSchema(spec: ProtoSpec): ProtoSchema {
|
|
253
|
+
const parsed = spec.files;
|
|
254
|
+
const messages = new Map(parsed.flatMap((f) => f.messages.map((m) => [m.fullName, m] as const)));
|
|
255
|
+
const enums = new Map(parsed.flatMap((f) => f.enums.map((e) => [e.fullName, e] as const)));
|
|
256
|
+
const resolve = (name: string, scopeName: string): string | undefined => {
|
|
257
|
+
if (name.startsWith('.')) return name.slice(1);
|
|
258
|
+
const parts = scopeName.split('.');
|
|
259
|
+
for (let n = parts.length; n >= 0; n--) {
|
|
260
|
+
const candidate = [...parts.slice(0, n), name].filter(Boolean).join('.');
|
|
261
|
+
if (messages.has(candidate) || enums.has(candidate)) return candidate;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
};
|
|
265
|
+
const kindOf = (type: string, scopeName: string): { type: string; kind: 'scalar' | 'message' | 'enum' } => {
|
|
266
|
+
if (SCALARS[type]) return { type, kind: 'scalar' };
|
|
267
|
+
const full = resolve(type, scopeName);
|
|
268
|
+
if (!full) throw new Error(`spec-ir-proto: ${scopeName}: type ${type} is not declared in the vendored files`);
|
|
269
|
+
return { type: full, kind: enums.has(full) ? 'enum' : 'message' };
|
|
270
|
+
};
|
|
271
|
+
const out: ProtoSchema = { messages: {}, enums: {}, rpcs: {} };
|
|
272
|
+
for (const [full, m] of messages) {
|
|
273
|
+
out.messages[full] = m.fields.map((f) => {
|
|
274
|
+
const t = kindOf(f.type, full);
|
|
275
|
+
const map = f.map ? (() => { const v = kindOf(f.map!.value, full); return { key: f.map!.key, value: v.type, valueKind: v.kind }; })() : undefined;
|
|
276
|
+
return { name: f.name, json: f.jsonName, number: f.number, type: t.type, kind: t.kind, ...(f.repeated ? { repeated: true as const } : {}), ...(map ? { map } : {}) };
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
for (const [full, e] of enums) out.enums[full] = e.numbers;
|
|
280
|
+
for (const svc of parsed.flatMap((f) => f.services)) {
|
|
281
|
+
const pkg = svc.fullName.split('.').slice(0, -1).join('.');
|
|
282
|
+
// an rpc's message type the tree does not declare is a spec that cannot be served, named, never an undefined schema
|
|
283
|
+
const resolved = (type: string): string => { const r = resolve(type, pkg); if (!r) throw new Error(`proto: ${svc.fullName} names message ${type}, which no file of the tree declares`); return r; };
|
|
284
|
+
for (const rpc of svc.rpcs) {
|
|
285
|
+
out.rpcs[`/${svc.fullName}/${rpc.name}`] = { operation: rpc.name, request: resolved(rpc.request), response: resolved(rpc.response), ...(rpc.serverStreams ? { serverStreams: true as const } : {}), ...(rpc.clientStreams ? { clientStreams: true as const } : {}) };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|