@kedge/server 0.0.0-stage → 0.9.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 +21 -0
- package/package.json +29 -3
- package/src/access.ts +472 -0
- package/src/admin-routes.ts +737 -0
- package/src/admin-shell.ts +72 -0
- package/src/app.ts +798 -0
- package/src/auth-routes.ts +381 -0
- package/src/capacity-routes.ts +440 -0
- package/src/entity-routes.ts +323 -0
- package/src/events.ts +84 -0
- package/src/http.ts +16 -0
- package/src/index.ts +2 -0
- package/src/meta-routes.ts +277 -0
- package/src/plugin-routes.ts +40 -0
- package/src/relay-routes.ts +192 -0
- package/src/track-routes.ts +102 -0
- package/src/types.ts +809 -0
- package/src/units/store.order.view.ts +9 -0
- package/src/units/store.product.view.ts +9 -0
- package/src/webhook-routes.ts +362 -0
- package/README.md +0 -3
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// P0.7's plugin entity routes (docs/proposals/p0.7-tracks-entities.md §4.4,
|
|
2
|
+
// §4.7): per declared entity, a windowed list, a search with classified
|
|
3
|
+
// values in the body, a read, a create, an update and an archive, each with
|
|
4
|
+
// its own zod schemas in the OpenAPI document; and the admin's page
|
|
5
|
+
// metadata. Row scopes are compiled into every batch and re-proved there, so
|
|
6
|
+
// the store authorizes each request itself (`store: true`).
|
|
7
|
+
import { createRoute, type OpenAPIHono, z } from "@hono/zod-openapi";
|
|
8
|
+
import { ENTITY_LIMITS, type EntityDefinition, type EntityField, holds, KedgeError, permissionsOf } from "@kedge/core";
|
|
9
|
+
import type { Context } from "hono";
|
|
10
|
+
import { type AdminPermissions, clientOf, type KedgeEnv } from "./access";
|
|
11
|
+
import { jsonBody, ok } from "./http";
|
|
12
|
+
import type { EntityAdmin } from "./types";
|
|
13
|
+
|
|
14
|
+
const Id = z.string().min(5).max(64);
|
|
15
|
+
const OrderId = z.string().regex(/^ord_[0-9A-Za-z]{10,40}$/);
|
|
16
|
+
|
|
17
|
+
/** A field's input schema (§4.7): strict, from the current declaration. */
|
|
18
|
+
function inputOf(f: EntityField): z.ZodTypeAny {
|
|
19
|
+
switch (f.type) {
|
|
20
|
+
case "string":
|
|
21
|
+
case "text":
|
|
22
|
+
return z.string().max(f.max ?? (f.type === "string" ? 1000 : 16000));
|
|
23
|
+
case "integer":
|
|
24
|
+
case "datetime":
|
|
25
|
+
return z.number().int();
|
|
26
|
+
case "boolean":
|
|
27
|
+
return z.boolean();
|
|
28
|
+
case "date":
|
|
29
|
+
return z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
30
|
+
case "select":
|
|
31
|
+
return z.enum(f.options as [string, ...string[]]);
|
|
32
|
+
case "json":
|
|
33
|
+
return z.record(z.string(), z.unknown());
|
|
34
|
+
case "staff":
|
|
35
|
+
case "location":
|
|
36
|
+
return z.string().min(1).max(64);
|
|
37
|
+
case "money":
|
|
38
|
+
return z.object({ amount: z.number().int(), currency_code: z.string().length(3) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A field's response schema (§4.7, codex r2): types only, so a value written
|
|
44
|
+
* under a widened rule still matches an older Worker's schema, and optional
|
|
45
|
+
* whenever its class can be projected away.
|
|
46
|
+
*/
|
|
47
|
+
function viewOf(f: EntityField): z.ZodTypeAny {
|
|
48
|
+
const t =
|
|
49
|
+
f.type === "integer" || f.type === "datetime"
|
|
50
|
+
? z.number().int()
|
|
51
|
+
: f.type === "boolean"
|
|
52
|
+
? z.boolean()
|
|
53
|
+
: f.type === "json"
|
|
54
|
+
? z.record(z.string(), z.unknown())
|
|
55
|
+
: f.type === "money"
|
|
56
|
+
? z.object({ amount: z.number().int(), currency_code: z.string() })
|
|
57
|
+
: z.string();
|
|
58
|
+
return t.nullable().optional();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function entityRoutes(
|
|
62
|
+
app: OpenAPIHono<KedgeEnv>,
|
|
63
|
+
perms: AdminPermissions,
|
|
64
|
+
entities: { store: EntityAdmin; declared: readonly { plugin: string; entity: string; def: EntityDefinition }[] },
|
|
65
|
+
): void {
|
|
66
|
+
const { store } = entities;
|
|
67
|
+
const ctx = (c: Context<KedgeEnv>) => c.var.access.ctx;
|
|
68
|
+
const client = (c: Context<KedgeEnv>) => {
|
|
69
|
+
const x = clientOf(c);
|
|
70
|
+
return { idempotencyKey: c.req.header("idempotency-key") ?? null, ip: x.ip, userAgent: x.userAgent };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
app.openapi(
|
|
74
|
+
perms.declare(
|
|
75
|
+
{ permission: "entity:meta", store: true },
|
|
76
|
+
createRoute({
|
|
77
|
+
method: "get",
|
|
78
|
+
path: "/admin/meta/entities",
|
|
79
|
+
responses: ok("The plugin entities the caller may read, as definitions"),
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
async (c) => c.json(store.metadata(ctx(c)) as object, 200),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const { plugin, entity, def } of entities.declared) {
|
|
86
|
+
const base = `/admin/plugins/${plugin}/${entity}`;
|
|
87
|
+
const p = permissionsOf(plugin, entity, def);
|
|
88
|
+
const fields = Object.entries(def.fields);
|
|
89
|
+
const view = z
|
|
90
|
+
.object({
|
|
91
|
+
id: z.string(),
|
|
92
|
+
version: z.number().int(),
|
|
93
|
+
state: z.enum(["live", "archived"]),
|
|
94
|
+
order_id: z.string().nullable(),
|
|
95
|
+
created_at: z.number().int(),
|
|
96
|
+
updated_at: z.number().int(),
|
|
97
|
+
created_by: z.string().optional(),
|
|
98
|
+
updated_by: z.string().optional(),
|
|
99
|
+
...Object.fromEntries(fields.map(([n, f]) => [n, viewOf(f)])),
|
|
100
|
+
})
|
|
101
|
+
.openapi(`${plugin}_${entity}`);
|
|
102
|
+
// Strict, as core's parser is: a misspelt field is refused, never dropped
|
|
103
|
+
// into an empty update that still steps the version (codex code r1).
|
|
104
|
+
const create = z.object({
|
|
105
|
+
values: z.strictObject(
|
|
106
|
+
Object.fromEntries(fields.map(([n, f]) => [n, f.required ? inputOf(f) : inputOf(f).nullable().optional()])),
|
|
107
|
+
),
|
|
108
|
+
order_id: OrderId.nullable().optional(),
|
|
109
|
+
});
|
|
110
|
+
const update = z.object({
|
|
111
|
+
version: z.number().int().min(1),
|
|
112
|
+
set: z.strictObject(
|
|
113
|
+
Object.fromEntries(
|
|
114
|
+
fields.map(([n, f]) => [n, f.required ? inputOf(f).optional() : inputOf(f).nullable().optional()]),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
});
|
|
118
|
+
// Public values only in the URL (P0.5 §5.2): `?<field>=v` (a comma list is `in`), `<field>_from`/`_to`.
|
|
119
|
+
const query = z.object({
|
|
120
|
+
sort: z.string().max(64).optional(),
|
|
121
|
+
state: z.enum(["live", "archived"]).optional(),
|
|
122
|
+
cursor: z.string().max(2048).optional(),
|
|
123
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
124
|
+
});
|
|
125
|
+
// A URL value is text; only a numeric field's operand becomes a number, so
|
|
126
|
+
// a text field compares as the stored text (`0010` isn't 10, codex code r10).
|
|
127
|
+
const typed = (field: string, v: string): unknown => {
|
|
128
|
+
const t = Object.hasOwn(def.fields, field) ? def.fields[field]?.type : undefined;
|
|
129
|
+
return (t === "integer" || t === "datetime") && v.trim() !== "" && !Number.isNaN(Number(v)) ? Number(v) : v;
|
|
130
|
+
};
|
|
131
|
+
const filtersOf = (q: Record<string, string>) => {
|
|
132
|
+
const out: { field: string; op: string; value: unknown }[] = [];
|
|
133
|
+
for (const [k, v] of Object.entries(q)) {
|
|
134
|
+
if (["sort", "state", "cursor", "limit"].includes(k)) continue;
|
|
135
|
+
// A declared field's own name is never a range: `available_from` is a field (codex code r4).
|
|
136
|
+
const range = Object.hasOwn(def.fields, k) ? null : /^(.+)_(from|to)$/.exec(k);
|
|
137
|
+
if (range)
|
|
138
|
+
out.push({
|
|
139
|
+
field: range[1] as string,
|
|
140
|
+
op: range[2] === "from" ? "gte" : "lt",
|
|
141
|
+
value: typed(range[1] as string, v),
|
|
142
|
+
});
|
|
143
|
+
else {
|
|
144
|
+
const values = v
|
|
145
|
+
.split(",")
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.map((x) => typed(k, x));
|
|
148
|
+
out.push(
|
|
149
|
+
values.length > 1 ? { field: k, op: "in", value: values } : { field: k, op: "eq", value: typed(k, v) },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
const sortOf = (s: string | undefined) =>
|
|
156
|
+
s
|
|
157
|
+
? { field: s.replace(/^-/, ""), direction: s.startsWith("-") ? ("desc" as const) : ("asc" as const) }
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
app.openapi(
|
|
161
|
+
perms.declare(
|
|
162
|
+
{ permission: p.read, store: true },
|
|
163
|
+
createRoute({ method: "get", path: base, request: { query }, responses: ok(`A page of ${def.label}`) }),
|
|
164
|
+
),
|
|
165
|
+
async (c) => {
|
|
166
|
+
const q = c.req.valid("query");
|
|
167
|
+
const sort = sortOf(q.sort);
|
|
168
|
+
return c.json(
|
|
169
|
+
(await store.list(ctx(c), plugin, entity, {
|
|
170
|
+
query: { via: "query", filters: filtersOf(c.req.query()), ...(sort ? { sort } : {}) },
|
|
171
|
+
cursor: q.cursor ?? null,
|
|
172
|
+
...(q.limit ? { limit: q.limit } : {}),
|
|
173
|
+
...(q.state ? { state: q.state } : {}),
|
|
174
|
+
})) as object,
|
|
175
|
+
200,
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
app.openapi(
|
|
181
|
+
perms.declare(
|
|
182
|
+
{ permission: p.read, store: true, read: true },
|
|
183
|
+
createRoute({
|
|
184
|
+
method: "post",
|
|
185
|
+
path: `${base}/search`,
|
|
186
|
+
request: jsonBody(
|
|
187
|
+
z.object({
|
|
188
|
+
filters: z
|
|
189
|
+
.array(z.object({ field: z.string(), op: z.enum(["eq", "in", "gte", "lt"]), value: z.unknown() }))
|
|
190
|
+
.max(16)
|
|
191
|
+
.optional(),
|
|
192
|
+
search: z
|
|
193
|
+
.object({
|
|
194
|
+
fields: z.array(z.string()).min(1).max(ENTITY_LIMITS.SEARCH_FIELDS_MAX),
|
|
195
|
+
term: z.string().min(1).max(200),
|
|
196
|
+
})
|
|
197
|
+
.optional(),
|
|
198
|
+
sort: z.string().max(64).optional(),
|
|
199
|
+
state: z.enum(["live", "archived"]).optional(),
|
|
200
|
+
cursor: z.string().max(2048).optional(),
|
|
201
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
responses: ok(`A page of ${def.label}, classified values in the body`),
|
|
205
|
+
}),
|
|
206
|
+
),
|
|
207
|
+
async (c) => {
|
|
208
|
+
const b = c.req.valid("json");
|
|
209
|
+
const sort = sortOf(b.sort);
|
|
210
|
+
return c.json(
|
|
211
|
+
(await store.list(ctx(c), plugin, entity, {
|
|
212
|
+
query: {
|
|
213
|
+
via: "body",
|
|
214
|
+
filters: b.filters ?? [],
|
|
215
|
+
...(b.search ? { search: b.search } : {}),
|
|
216
|
+
...(sort ? { sort } : {}),
|
|
217
|
+
},
|
|
218
|
+
cursor: b.cursor ?? null,
|
|
219
|
+
...(b.limit ? { limit: b.limit } : {}),
|
|
220
|
+
...(b.state ? { state: b.state } : {}),
|
|
221
|
+
})) as object,
|
|
222
|
+
200,
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
app.openapi(
|
|
228
|
+
perms.declare(
|
|
229
|
+
{ permission: p.read, store: true },
|
|
230
|
+
createRoute({
|
|
231
|
+
method: "get",
|
|
232
|
+
path: `${base}/{id}`,
|
|
233
|
+
request: { params: z.object({ id: Id }) },
|
|
234
|
+
responses: { 200: { description: `One ${def.label}`, content: { "application/json": { schema: view } } } },
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
async (c) => {
|
|
238
|
+
if (!holds(ctx(c).grants, p.read))
|
|
239
|
+
throw new KedgeError("FORBIDDEN", "You don't have permission for this", { permission: p.read });
|
|
240
|
+
const row = await store.get(ctx(c), plugin, entity, c.req.valid("param").id);
|
|
241
|
+
if (!row) throw new KedgeError("NOT_FOUND", "Not found");
|
|
242
|
+
return c.json(row as never, 200);
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
app.openapi(
|
|
247
|
+
perms.declare(
|
|
248
|
+
{ permission: p.write, store: true },
|
|
249
|
+
createRoute({
|
|
250
|
+
method: "post",
|
|
251
|
+
path: base,
|
|
252
|
+
request: jsonBody(create),
|
|
253
|
+
responses: ok(`The new ${def.label}, or a receipt`),
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
async (c) => {
|
|
257
|
+
const b = c.req.valid("json");
|
|
258
|
+
return c.json(
|
|
259
|
+
(await store.create(ctx(c), {
|
|
260
|
+
plugin,
|
|
261
|
+
entity,
|
|
262
|
+
values: b.values,
|
|
263
|
+
orderId: b.order_id ?? null,
|
|
264
|
+
...client(c),
|
|
265
|
+
})) as object,
|
|
266
|
+
200,
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
app.openapi(
|
|
272
|
+
perms.declare(
|
|
273
|
+
{ permission: p.write, store: true },
|
|
274
|
+
createRoute({
|
|
275
|
+
method: "patch",
|
|
276
|
+
path: `${base}/{id}`,
|
|
277
|
+
request: { params: z.object({ id: Id }), ...jsonBody(update) },
|
|
278
|
+
responses: ok(`The updated ${def.label}, or a receipt`),
|
|
279
|
+
}),
|
|
280
|
+
),
|
|
281
|
+
async (c) => {
|
|
282
|
+
const b = c.req.valid("json");
|
|
283
|
+
return c.json(
|
|
284
|
+
(await store.write(ctx(c), {
|
|
285
|
+
plugin,
|
|
286
|
+
entity,
|
|
287
|
+
id: c.req.valid("param").id,
|
|
288
|
+
version: b.version,
|
|
289
|
+
set: b.set,
|
|
290
|
+
...client(c),
|
|
291
|
+
})) as object,
|
|
292
|
+
200,
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
app.openapi(
|
|
298
|
+
perms.declare(
|
|
299
|
+
{ permission: p.write, store: true },
|
|
300
|
+
createRoute({
|
|
301
|
+
method: "post",
|
|
302
|
+
path: `${base}/{id}/archive`,
|
|
303
|
+
request: { params: z.object({ id: Id }), ...jsonBody(z.object({ version: z.number().int().min(1) })) },
|
|
304
|
+
responses: ok(`The archived ${def.label}, or a receipt`),
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
async (c) => {
|
|
308
|
+
const b = c.req.valid("json");
|
|
309
|
+
return c.json(
|
|
310
|
+
(await store.write(ctx(c), {
|
|
311
|
+
plugin,
|
|
312
|
+
entity,
|
|
313
|
+
id: c.req.valid("param").id,
|
|
314
|
+
version: b.version,
|
|
315
|
+
archive: true,
|
|
316
|
+
...client(c),
|
|
317
|
+
})) as object,
|
|
318
|
+
200,
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The event catalog's payload schemas (docs/05 §9; p0.6a §2.1), one per type
|
|
2
|
+
// P0.6a emits or delivers, matching what 0023–0026's triggers and the stores
|
|
3
|
+
// write. Core keeps each type's aggregate and restore-stability
|
|
4
|
+
// (EVENT_CATALOG); this is the shape a subscriber, the SDK and the docs see.
|
|
5
|
+
import { z } from "@hono/zod-openapi";
|
|
6
|
+
import { EVENT_CATALOG, type EventType } from "@kedge/core";
|
|
7
|
+
|
|
8
|
+
const orderTransition = z.object({ order_id: z.string(), transition_uid: z.string() });
|
|
9
|
+
|
|
10
|
+
export const EVENT_SCHEMAS = {
|
|
11
|
+
"order.created": z.object({ id: z.string() }).passthrough(),
|
|
12
|
+
"order.placed": z
|
|
13
|
+
.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
number: z.number().int().nullable(),
|
|
16
|
+
status: z.string(),
|
|
17
|
+
currency_code: z.string(),
|
|
18
|
+
// No email since 0027 (p0.6b §2.6): it is read from the order. Events
|
|
19
|
+
// written before keep theirs, which `passthrough` lets through.
|
|
20
|
+
subtotal: z.number().int(),
|
|
21
|
+
total: z.number().int(),
|
|
22
|
+
revenue_total: z.number().int().nullable(),
|
|
23
|
+
deposit_total: z.number().int().nullable(),
|
|
24
|
+
placed_at: z.number().int().nullable(),
|
|
25
|
+
lines: z.array(
|
|
26
|
+
z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
variant_id: z.string(),
|
|
29
|
+
sku: z.string(),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
unit_price: z.number().int(),
|
|
32
|
+
quantity: z.number().int(),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
.passthrough(),
|
|
37
|
+
"order.expired": orderTransition,
|
|
38
|
+
"order.canceled": orderTransition,
|
|
39
|
+
"order.completed": orderTransition,
|
|
40
|
+
"timer.fired": z.object({
|
|
41
|
+
timer_id: z.string(),
|
|
42
|
+
key: z.string(),
|
|
43
|
+
generation: z.number().int(),
|
|
44
|
+
revision_id: z.string(),
|
|
45
|
+
}),
|
|
46
|
+
"pool.capacity_changed": z.object({ pool_id: z.string() }).passthrough(),
|
|
47
|
+
"level.availability_changed": z
|
|
48
|
+
.object({
|
|
49
|
+
inventory_item_id: z.string(),
|
|
50
|
+
location_id: z.string(),
|
|
51
|
+
on_hand: z.number().int(),
|
|
52
|
+
safety_stock: z.number().int(),
|
|
53
|
+
})
|
|
54
|
+
.passthrough(),
|
|
55
|
+
"capacity.base_negative": z.object({ pool_id: z.string() }).passthrough(),
|
|
56
|
+
"stock.shortage": z.object({}).passthrough(),
|
|
57
|
+
// P0.6b (§2.6, §3.2).
|
|
58
|
+
"webhook_endpoint.disabled": z.object({
|
|
59
|
+
endpoint_id: z.string(),
|
|
60
|
+
reason: z.enum(["failing", "staff"]),
|
|
61
|
+
disabled_at: z.number().int(),
|
|
62
|
+
generation: z.number().int(),
|
|
63
|
+
}),
|
|
64
|
+
"webhook.test": z.object({ endpoint_id: z.string(), sent_at: z.number().int() }),
|
|
65
|
+
"store_calendar.changed": z.object({ version: z.number().int() }),
|
|
66
|
+
// P0.7 (§3.9): ids only, never the comment.
|
|
67
|
+
"track.transitioned": z.object({
|
|
68
|
+
plugin: z.string(),
|
|
69
|
+
track: z.string(),
|
|
70
|
+
aggregate: z.string(),
|
|
71
|
+
aggregate_id: z.string(),
|
|
72
|
+
track_state_id: z.string(),
|
|
73
|
+
from: z.string(),
|
|
74
|
+
to: z.string(),
|
|
75
|
+
version: z.number().int(),
|
|
76
|
+
transition_uid: z.string(),
|
|
77
|
+
}),
|
|
78
|
+
} satisfies Record<EventType, z.ZodTypeAny>;
|
|
79
|
+
|
|
80
|
+
/** Every catalog type has a schema, and no schema names a type core doesn't know. */
|
|
81
|
+
export function catalogComplete(): boolean {
|
|
82
|
+
const core = Object.keys(EVENT_CATALOG).sort();
|
|
83
|
+
return JSON.stringify(core) === JSON.stringify(Object.keys(EVENT_SCHEMAS).sort());
|
|
84
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Route description helpers shared by the route modules.
|
|
2
|
+
import { z } from "@hono/zod-openapi";
|
|
3
|
+
|
|
4
|
+
const ErrorSchema = z.object({
|
|
5
|
+
error: z.object({ code: z.string(), message: z.string(), details: z.record(z.string(), z.unknown()).optional() }),
|
|
6
|
+
});
|
|
7
|
+
const errors = { 400: { description: "Invalid request", content: { "application/json": { schema: ErrorSchema } } } };
|
|
8
|
+
|
|
9
|
+
export const ok = (description: string) => ({
|
|
10
|
+
200: { description, content: { "application/json": { schema: z.any() } } },
|
|
11
|
+
...errors,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const jsonBody = <T extends z.ZodType>(schema: T) => ({
|
|
15
|
+
body: { content: { "application/json": { schema } }, required: true },
|
|
16
|
+
});
|
package/src/index.ts
ADDED