ebely 0.0.1 → 0.0.2

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/dist/index.js ADDED
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var evely_exports = {};
22
+ __export(evely_exports, {
23
+ ApiResponse: () => ApiResponse,
24
+ BaseStore: () => BaseStore,
25
+ EbelyAssertionError: () => EbelyAssertionError,
26
+ HookRegistry: () => HookRegistry,
27
+ generateClient: () => generateClient
28
+ });
29
+ module.exports = __toCommonJS(evely_exports);
30
+
31
+ // src/base-store.ts
32
+ var BaseStore = class {
33
+ store = /* @__PURE__ */ new Map();
34
+ /**
35
+ * Типизированный доступ к эндпоинтам ИЗНУТРИ методов-сценариев
36
+ * («actions»): `this.api.<группа>.<метод>(...)`. Само значение
37
+ * подставляет генерируемый `World` (для user-store — клиент этого
38
+ * пользователя, для world-store — анонимный клиент), поэтому здесь это
39
+ * объявление без инициализатора. Тип `Api` пользователь задаёт вторым
40
+ * дженериком, передавая сгенерированный `WorldApi`:
41
+ *
42
+ * import type { WorldApi } from './generated'
43
+ * class UserStore extends BaseStore<Vars, WorldApi> {
44
+ * async fullRegister(args: { email: string; password: string }) {
45
+ * await this.api.auth.register({ body: args })
46
+ * await this.api.auth.confirm({ body: { code: '0000' } })
47
+ * }
48
+ * }
49
+ *
50
+ * (Тип импортируется как `import type` → рантайм-цикла нет, ровно как
51
+ * у `hooks.ts`; см. ARCHITECTURE.md §7–§8.)
52
+ */
53
+ api;
54
+ /** Сохранить внутреннюю переменную пользователя. */
55
+ set(args) {
56
+ this.store.set(args.key, args.value);
57
+ }
58
+ /** Прочитать внутреннюю переменную пользователя (undefined, если не задана). */
59
+ get(args) {
60
+ return this.store.get(args.key);
61
+ }
62
+ };
63
+
64
+ // src/response.ts
65
+ var EbelyAssertionError = class extends Error {
66
+ constructor(message) {
67
+ super(message);
68
+ this.name = "EbelyAssertionError";
69
+ }
70
+ };
71
+ function safeJson(value) {
72
+ try {
73
+ return JSON.stringify(value) ?? String(value);
74
+ } catch {
75
+ return String(value);
76
+ }
77
+ }
78
+ function matchPartial(args) {
79
+ const { actual, expected, path = "" } = args;
80
+ if (Array.isArray(expected)) {
81
+ if (!Array.isArray(actual))
82
+ return { path, expected, actual };
83
+ for (let i = 0; i < expected.length; i++) {
84
+ const m = matchPartial({
85
+ actual: actual[i],
86
+ expected: expected[i],
87
+ path: `${path}[${i}]`
88
+ });
89
+ if (m)
90
+ return m;
91
+ }
92
+ return null;
93
+ }
94
+ if (expected !== null && typeof expected === "object") {
95
+ if (actual === null || typeof actual !== "object" || Array.isArray(actual)) {
96
+ return { path, expected, actual };
97
+ }
98
+ const exp = expected;
99
+ const act = actual;
100
+ for (const key of Object.keys(exp)) {
101
+ const m = matchPartial({
102
+ actual: act[key],
103
+ expected: exp[key],
104
+ path: path ? `${path}.${key}` : key
105
+ });
106
+ if (m)
107
+ return m;
108
+ }
109
+ return null;
110
+ }
111
+ return Object.is(actual, expected) ? null : { path, expected, actual };
112
+ }
113
+ function assertResponse(args) {
114
+ const { actualStatus, actualBody, expectedStatus, expectedBody } = args;
115
+ if (actualStatus !== expectedStatus) {
116
+ throw new EbelyAssertionError(
117
+ `\u041E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441 ${expectedStatus}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D ${actualStatus}.
118
+ \u0422\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430: ${safeJson(actualBody)}`
119
+ );
120
+ }
121
+ if (expectedBody === void 0)
122
+ return;
123
+ const mismatch = matchPartial({ actual: actualBody, expected: expectedBody });
124
+ if (mismatch) {
125
+ throw new EbelyAssertionError(
126
+ `\u0422\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u043F\u0443\u0442\u0438 "${mismatch.path || "<root>"}": \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C ${safeJson(mismatch.expected)}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E ${safeJson(mismatch.actual)}.
127
+ \u041F\u043E\u043B\u043D\u043E\u0435 \u0442\u0435\u043B\u043E: ${safeJson(actualBody)}`
128
+ );
129
+ }
130
+ }
131
+ var ApiResponse = class {
132
+ /** Фактический HTTP-статус ответа. */
133
+ status;
134
+ /** Распарсенное тело ответа. */
135
+ body;
136
+ constructor(args) {
137
+ this.status = args.status;
138
+ this.body = args.body;
139
+ }
140
+ /**
141
+ * Проверяет статус и (опционально) часть тела ответа.
142
+ *
143
+ * @param status Ожидаемый статус. Подсказывается интеллисенсом из
144
+ * swagger; незадекларированный литерал — ошибка типов. Чтобы
145
+ * проверить статус, которого нет в схеме (например `400`),
146
+ * используйте `res.assert(400 as any, ...)`.
147
+ * @param expectedBody Необязательная часть тела: проверяются только
148
+ * переданные поля (глубоко-частично), остальные игнорируются.
149
+ * @returns тот же объект ответа — удобно читать `.body` после проверки.
150
+ */
151
+ assert(status, expectedBody) {
152
+ assertResponse({
153
+ actualStatus: this.status,
154
+ actualBody: this.body,
155
+ expectedStatus: status,
156
+ expectedBody
157
+ });
158
+ return this;
159
+ }
160
+ };
161
+
162
+ // src/hooks.ts
163
+ var HookRegistry = class {
164
+ beforeMap = /* @__PURE__ */ new Map();
165
+ afterMap = /* @__PURE__ */ new Map();
166
+ /** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
167
+ before(args) {
168
+ const list = this.beforeMap.get(args.key) ?? [];
169
+ list.push(args.fn);
170
+ this.beforeMap.set(args.key, list);
171
+ }
172
+ /** Зарегистрировать `after`-хук на ключ `"<группа>.<метод>"`. */
173
+ after(args) {
174
+ const list = this.afterMap.get(args.key) ?? [];
175
+ list.push(args.fn);
176
+ this.afterMap.set(args.key, list);
177
+ }
178
+ /** Прогнать все `before`-хуки ключа по очереди (await на async). */
179
+ async runBefore(args) {
180
+ for (const fn of this.beforeMap.get(args.key) ?? []) {
181
+ await fn({ request: args.request, ctx: args.ctx });
182
+ }
183
+ }
184
+ /** Прогнать все `after`-хуки ключа по очереди (await на async). */
185
+ async runAfter(args) {
186
+ for (const fn of this.afterMap.get(args.key) ?? []) {
187
+ await fn({ request: args.request, response: args.response, ctx: args.ctx });
188
+ }
189
+ }
190
+ };
191
+
192
+ // src/generate-client.ts
193
+ var import_promises2 = require("fs/promises");
194
+ var import_node_path2 = require("path");
195
+
196
+ // src/generator/schema.ts
197
+ function schemaToType(args) {
198
+ const { schema, spec, indent = 0 } = args;
199
+ if (!schema)
200
+ return "unknown";
201
+ if (typeof schema.$ref === "string") {
202
+ const resolved = resolveRef({ ref: schema.$ref, spec });
203
+ return schemaToType({ schema: resolved, spec, indent });
204
+ }
205
+ for (const key of ["allOf", "oneOf", "anyOf"]) {
206
+ if (Array.isArray(schema[key])) {
207
+ const joiner = key === "allOf" ? " & " : " | ";
208
+ return schema[key].map((s) => schemaToType({ schema: s, spec, indent })).join(joiner);
209
+ }
210
+ }
211
+ if (Array.isArray(schema.enum)) {
212
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
213
+ }
214
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
215
+ const rendered = types.map((type) => {
216
+ switch (type) {
217
+ case "string":
218
+ return "string";
219
+ case "number":
220
+ case "integer":
221
+ return "number";
222
+ case "boolean":
223
+ return "boolean";
224
+ case "null":
225
+ return "null";
226
+ case "array":
227
+ return `Array<${schemaToType({ schema: schema.items, spec, indent })}>`;
228
+ case "object": {
229
+ const props = schema.properties ?? {};
230
+ const required = schema.required ?? [];
231
+ const keys = Object.keys(props);
232
+ if (keys.length === 0)
233
+ return "Record<string, unknown>";
234
+ const pad = " ".repeat(indent + 1);
235
+ const closePad = " ".repeat(indent);
236
+ const lines = keys.map((k) => {
237
+ const optional = required.includes(k) ? "" : "?";
238
+ const valueType = schemaToType({ schema: props[k], spec, indent: indent + 1 });
239
+ return `${pad}${JSON.stringify(k)}${optional}: ${valueType}`;
240
+ });
241
+ return `{
242
+ ${lines.join("\n")}
243
+ ${closePad}}`;
244
+ }
245
+ default:
246
+ return "unknown";
247
+ }
248
+ });
249
+ return rendered.join(" | ");
250
+ }
251
+ function resolveRef(args) {
252
+ const { ref, spec } = args;
253
+ if (!ref.startsWith("#/"))
254
+ return void 0;
255
+ return ref.slice(2).split("/").reduce((acc, part) => acc ? acc[part] : void 0, spec);
256
+ }
257
+ function pickResponseSchema(args) {
258
+ const { responses } = args;
259
+ const status = Object.keys(responses ?? {}).find((s) => s.startsWith("2")) ?? "default";
260
+ return responses?.[status]?.content?.["application/json"]?.schema;
261
+ }
262
+ function collectResponseSchemas(args) {
263
+ const { responses } = args;
264
+ const out = [];
265
+ for (const key of Object.keys(responses ?? {})) {
266
+ const status = Number(key);
267
+ if (!Number.isInteger(status))
268
+ continue;
269
+ out.push({
270
+ status,
271
+ schema: responses[key]?.content?.["application/json"]?.schema
272
+ });
273
+ }
274
+ return out;
275
+ }
276
+
277
+ // src/generator/operations.ts
278
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
279
+ function collectOperations(args) {
280
+ const { spec } = args;
281
+ const operations = [];
282
+ for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
283
+ for (const method of HTTP_METHODS) {
284
+ const op = pathItem[method];
285
+ if (!op)
286
+ continue;
287
+ const operationId = op.operationId ?? `${method}${path}`;
288
+ const dotIndex = operationId.indexOf(".");
289
+ const group = dotIndex === -1 ? "default" : operationId.slice(0, dotIndex);
290
+ const name = dotIndex === -1 ? operationId : operationId.slice(dotIndex + 1);
291
+ const parameters = op.parameters ?? [];
292
+ const pathParams = parameters.filter((p) => p.in === "path").map((p) => p.name);
293
+ const queryParams = parameters.filter((p) => p.in === "query").map((p) => p.name);
294
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
295
+ const responseSchema = pickResponseSchema({ responses: op.responses, spec });
296
+ const responseType = schemaToType({ schema: responseSchema, spec, indent: 3 });
297
+ const declared = collectResponseSchemas({ responses: op.responses, spec });
298
+ const responses = declared.length > 0 ? declared.map((r) => ({
299
+ status: r.status,
300
+ bodyType: schemaToType({ schema: r.schema, spec, indent: 4 })
301
+ })) : [{ status: 200, bodyType: responseType }];
302
+ operations.push({
303
+ group,
304
+ name,
305
+ method,
306
+ path,
307
+ pathParams,
308
+ queryParams,
309
+ bodyType: bodySchema ? schemaToType({ schema: bodySchema, spec, indent: 4 }) : null,
310
+ bodyRequired: Boolean(op.requestBody?.required),
311
+ responseType,
312
+ responses,
313
+ summary: op.summary
314
+ });
315
+ }
316
+ }
317
+ return operations;
318
+ }
319
+
320
+ // src/generator/render.ts
321
+ function buildInputType(op) {
322
+ const parts = [];
323
+ if (op.pathParams.length > 0) {
324
+ const fields = op.pathParams.map((p) => `${JSON.stringify(p)}: string`).join("; ");
325
+ parts.push(`path: { ${fields} }`);
326
+ }
327
+ if (op.queryParams.length > 0) {
328
+ const fields = op.queryParams.map((p) => `${JSON.stringify(p)}?: string | number | boolean`).join("; ");
329
+ parts.push(`query?: { ${fields} }`);
330
+ }
331
+ if (op.bodyType) {
332
+ parts.push(`body${op.bodyRequired ? "" : "?"}: ${op.bodyType}`);
333
+ }
334
+ if (parts.length === 0)
335
+ return { type: "{}", optional: true };
336
+ const optional = op.pathParams.length === 0 && (!op.bodyType || !op.bodyRequired);
337
+ return { type: `{ ${parts.join("; ")} }`, optional };
338
+ }
339
+ function buildStatusMapType(op) {
340
+ const entries = op.responses.map((r) => `${r.status}: ${r.bodyType}`);
341
+ return `{ ${entries.join("; ")} }`;
342
+ }
343
+ function methodReturnType(args) {
344
+ const { op, mode } = args;
345
+ if (mode === "frontend")
346
+ return `Promise<${op.responseType}>`;
347
+ return `Promise<ApiResponse<${buildStatusMapType(op)}>>`;
348
+ }
349
+ function hookKey(op) {
350
+ return `${op.group}.${op.name}`;
351
+ }
352
+ function hookBodyType(op) {
353
+ return op.bodyType ?? "undefined";
354
+ }
355
+ function renderMethod(args) {
356
+ const { op, mode } = args;
357
+ const { type, optional } = buildInputType(op);
358
+ const inputParam = `input${optional ? "?" : ""}: ${type}`;
359
+ const doc = op.summary ? `
360
+ /** ${op.summary} */` : "";
361
+ const call = `request({
362
+ method: ${JSON.stringify(op.method.toUpperCase())},
363
+ path: ${JSON.stringify(op.path)},
364
+ opKey: ${JSON.stringify(hookKey(op))},
365
+ input: input as RequestInput,
366
+ })`;
367
+ if (mode === "frontend") {
368
+ return `${doc}
369
+ ${JSON.stringify(op.name)}: (${inputParam}): Promise<${op.responseType}> =>
370
+ ${call} as Promise<${op.responseType}>,`;
371
+ }
372
+ const map = buildStatusMapType(op);
373
+ return `${doc}
374
+ ${JSON.stringify(op.name)}: async (${inputParam}): Promise<ApiResponse<${map}>> => {
375
+ const res = await ${call}
376
+ return new ApiResponse(res) as unknown as ApiResponse<${map}>
377
+ },`;
378
+ }
379
+ function renderImports(args) {
380
+ const { mode, userStoreImport, configImport } = args;
381
+ const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
382
+ return `import { ${values} } from ${JSON.stringify(userStoreImport)}
383
+ import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
384
+ import { ebely } from ${JSON.stringify(configImport)}`;
385
+ }
386
+ function renderRequestTail(mode) {
387
+ if (mode === "frontend") {
388
+ return {
389
+ returnType: "unknown",
390
+ ret: ` if (!response.ok) {
391
+ throw new Error(
392
+ \`Request \${method} \${resolvedPath} failed with \${response.status}: \${text}\`,
393
+ )
394
+ }
395
+
396
+ return data`
397
+ };
398
+ }
399
+ return {
400
+ returnType: "{ status: number; body: unknown }",
401
+ ret: ` return { status: response.status, body: data }`
402
+ };
403
+ }
404
+ function groupOperations(operations) {
405
+ const groups = /* @__PURE__ */ new Map();
406
+ for (const op of operations) {
407
+ const list = groups.get(op.group) ?? [];
408
+ list.push(op);
409
+ groups.set(op.group, list);
410
+ }
411
+ return groups;
412
+ }
413
+ function renderApiType(args) {
414
+ const { groups, mode } = args;
415
+ const blocks = [...groups.entries()].map(([group, ops]) => {
416
+ const leaves = ops.map((op) => {
417
+ const { type, optional } = buildInputType(op);
418
+ const inputParam = `input${optional ? "?" : ""}: ${type}`;
419
+ return ` ${JSON.stringify(op.name)}: (${inputParam}) => ${methodReturnType({ op, mode })}`;
420
+ });
421
+ return ` ${JSON.stringify(group)}: {
422
+ ${leaves.join("\n")}
423
+ }`;
424
+ });
425
+ return `export type WorldApi = {
426
+ ${blocks.join("\n")}
427
+ }`;
428
+ }
429
+ function renderHookTreeType(groups) {
430
+ const blocks = [...groups.entries()].map(([group, ops]) => {
431
+ const leaves = ops.map((op) => {
432
+ const body = hookBodyType(op);
433
+ return ` ${JSON.stringify(op.name)}: {
434
+ before(fn: BeforeHook<Store, ${body}>): void
435
+ after(fn: AfterHook<Store, ${body}, ${op.responseType}>): void
436
+ }`;
437
+ });
438
+ return ` ${JSON.stringify(group)}: {
439
+ ${leaves.join("\n")}
440
+ }`;
441
+ });
442
+ return `type EbelyHookTree<Store extends BaseStore> = {
443
+ ${blocks.join("\n")}
444
+ }`;
445
+ }
446
+ function renderHookTreeBuilder(groups) {
447
+ const blocks = [...groups.entries()].map(([group, ops]) => {
448
+ const leaves = ops.map((op) => {
449
+ const key = JSON.stringify(hookKey(op));
450
+ return ` ${JSON.stringify(op.name)}: {
451
+ before: (fn: BeforeHook<Store>) => r.before({ key: ${key}, fn }),
452
+ after: (fn: AfterHook<Store>) => r.after({ key: ${key}, fn }),
453
+ },`;
454
+ });
455
+ return ` ${JSON.stringify(group)}: {
456
+ ${leaves.join("\n")}
457
+ },`;
458
+ });
459
+ return ` private buildHookTree(): EbelyHookTree<Store> {
460
+ const r = this.hookRegistry
461
+ return {
462
+ ${blocks.join("\n")}
463
+ } as unknown as EbelyHookTree<Store>
464
+ }`;
465
+ }
466
+ function renderApiTreeBuilder(args) {
467
+ const { groups, mode } = args;
468
+ const groupBlocks = [...groups.entries()].map(([group, ops]) => {
469
+ const methods = ops.map((op) => renderMethod({ op, mode }));
470
+ return ` ${JSON.stringify(group)}: {
471
+ ${methods.join("\n")}
472
+ },`;
473
+ });
474
+ return ` /** \u0414\u0435\u0440\u0435\u0432\u043E \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u043E\u0432 \u043F\u043E\u0432\u0435\u0440\u0445 \u043E\u0434\u043D\u043E\u0433\u043E \`request\`. */
475
+ private buildApiTree(request: RequestFn) {
476
+ return {
477
+ ${groupBlocks.join("\n")}
478
+ }
479
+ }`;
480
+ }
481
+ function renderClient(args) {
482
+ const { spec, operations, mode, userStoreImport, configImport } = args;
483
+ const basePath = spec.servers?.[0]?.url ?? "";
484
+ const groups = groupOperations(operations);
485
+ const tail = renderRequestTail(mode);
486
+ return `// AUTO-GENERATED by ebely (generateClient) \u2014 \u041D\u0415 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.
487
+ // \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
488
+ // \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
489
+ // \u041F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F: pnpm run client:generate
490
+
491
+ ${renderImports({ mode, userStoreImport, configImport })}
492
+
493
+ type RequestInput = {
494
+ path?: Record<string, string>
495
+ query?: Record<string, string | number | boolean | undefined>
496
+ body?: unknown
497
+ }
498
+
499
+ type RequestFn = (req: {
500
+ method: string
501
+ path: string
502
+ opKey: string
503
+ input?: RequestInput
504
+ }) => Promise<${tail.returnType}>
505
+
506
+ export type CreateUserArgs = {
507
+ /** \u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043A\u043E \u0432\u0441\u0435\u043C \u0437\u0430\u043F\u0440\u043E\u0441\u0430\u043C \u044D\u0442\u043E\u0433\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F. */
508
+ headers?: Record<string, string>
509
+ }
510
+
511
+ ${renderApiType({ groups, mode })}
512
+
513
+ ${renderHookTreeType(groups)}
514
+
515
+ /**
516
+ * \u0422\u0438\u043F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0445\u0443\u043A\u043E\u0432 \u0434\u043B\u044F \u042D\u0422\u041E\u0413\u041E \u0431\u044D\u043A\u0435\u043D\u0434\u0430. \u041E\u0431\u044A\u044F\u0432\u0438\u0442\u0435 \u0445\u0443\u043A\u0438 \u0432 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u043C
517
+ * \u0444\u0430\u0439\u043B\u0435 \u0438 \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435 \u043E\u0434\u043D\u043E\u0439 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 \u0432 \u043A\u043E\u043D\u0444\u0438\u0433 (\`EbelyConfig.hooks\`).
518
+ * \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0421\u0412\u041E\u0419 \u043A\u043B\u0430\u0441\u0441 store \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u043C, \u0447\u0442\u043E\u0431\u044B \`ctx\` \u0431\u044B\u043B \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D:
519
+ *
520
+ * import type { UserStore } from './userStore'
521
+ * export const hooks: Hooks<UserStore> = (h) => {
522
+ * h.posts.create.after(({ response, ctx }) => { \u2026 })
523
+ * }
524
+ *
525
+ * (\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u041D\u0415 \u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u0438\u0437 \`ebely.userStore\` \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E: \u044D\u0442\u043E \u0441\u043E\u0437\u0434\u0430\u043B\u043E
526
+ * \u0431\u044B \u0446\u0438\u043A\u043B \u0442\u0438\u043F\u043E\u0432 \`ebely\` \u21C4 \`Hooks\`, \u0442.\u043A. \`hooks\` \u043B\u0435\u0436\u0438\u0442 \u0432\u043D\u0443\u0442\u0440\u0438 \`ebely\`.)
527
+ */
528
+ export type Hooks<Store extends BaseStore = BaseStore> = (
529
+ h: EbelyHookTree<Store>,
530
+ ) => void
531
+
532
+ /**
533
+ * \u0411\u0430\u0437\u0430 \`World\` \u2014 \u044D\u0442\u043E \u0441\u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \`ebely.worldStore\` (\u0438\u043B\u0438 \u043F\u0443\u0441\u0442\u043E\u0439
534
+ * \`BaseStore\`, \u0435\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D). \u041F\u043E\u044D\u0442\u043E\u043C\u0443 \`world.<\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439>()\` \u0438
535
+ * \`world.get/set\` \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u0438 \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u044B \u0440\u043E\u0432\u043D\u043E \u043A\u0430\u043A \u0443 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F,
536
+ * \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0431\u043B\u0430\u0441\u0442\u044C \u2014 \u0432\u0435\u0441\u044C \u043C\u0438\u0440. \u0422\u0438\u043F \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0438\u0437 \`ebely\` \u0442\u0435\u043C \u0436\u0435 \u043F\u0440\u0438\u0451\u043C\u043E\u043C, \u0447\u0442\u043E
537
+ * \u0438 \`Store\` (\u043D\u0438\u043A\u0430\u043A\u0438\u0445 \u0440\u0430\u043D\u0442\u0430\u0439\u043C-\u0443\u0441\u043B\u043E\u0432\u0438\u0439 \u2014 \u0441\u043C. ARCHITECTURE.md \xA78).
538
+ */
539
+ type ConfiguredWorldStore =
540
+ typeof ebely extends { worldStore: new () => infer I extends BaseStore }
541
+ ? I
542
+ : BaseStore
543
+ const WorldStoreBase = ((ebely as { worldStore?: new () => BaseStore })
544
+ .worldStore ?? BaseStore) as new () => ConfiguredWorldStore
545
+
546
+ export class World<
547
+ Store extends BaseStore = InstanceType<typeof ebely.userStore>,
548
+ > extends WorldStoreBase {
549
+ /**
550
+ * \u041E\u0431\u0449\u0438\u0439 \u0440\u0435\u0435\u0441\u0442\u0440 \u0445\u0443\u043A\u043E\u0432. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u2014 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0430\u044F (\u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430),
551
+ * \u043D\u043E \`ctx\` \u043F\u043E\u0434\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432 \u043C\u043E\u043C\u0435\u043D\u0442 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 = store \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u044E\u0437\u0435\u0440\u0430.
552
+ */
553
+ private hookRegistry = new HookRegistry()
554
+
555
+ constructor(
556
+ public args: {
557
+ /** URL \u0431\u044D\u043A\u0435\u043D\u0434\u0430. \u0415\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F ebely.url \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430. */
558
+ url?: string
559
+ /** \u041A\u043B\u0430\u0441\u0441-\u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435. \u0415\u0441\u043B\u0438 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F ebely.userStore. */
560
+ store?: new () => Store
561
+ } = {},
562
+ ) {
563
+ super()
564
+ const registrar = (ebely as { hooks?: (h: unknown) => void }).hooks
565
+ if (registrar) registrar(this.buildHookTree())
566
+ // world-store \u0445\u043E\u0434\u0438\u0442 \u0410\u041D\u041E\u041D\u0418\u041C\u041D\u042B\u041C \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C (\u0431\u0435\u0437 per-user \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432);
567
+ // ctx \u0445\u0443\u043A\u043E\u0432 \u0434\u043B\u044F \u0442\u0430\u043A\u0438\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 = \u0441\u0430\u043C world.
568
+ ;(this as unknown as { api: unknown }).api = this.buildApiTree(
569
+ this.makeRequest({ headers: {}, store: this }),
570
+ )
571
+ }
572
+
573
+ /** \u0411\u0430\u0437\u043E\u0432\u044B\u0439 URL \u0441 \u0443\u0447\u0451\u0442\u043E\u043C server.url \u0438\u0437 \u0441\u0445\u0435\u043C\u044B. */
574
+ private baseUrl(): string {
575
+ return (this.args.url ?? ebely.url).replace(/\\/$/, '') + ${JSON.stringify(basePath)}
576
+ }
577
+
578
+ ${renderHookTreeBuilder(groups)}
579
+
580
+ /**
581
+ * \u0421\u043E\u0437\u0434\u0430\u0451\u0442 \`request\`, \u0437\u0430\u043C\u043A\u043D\u0443\u0442\u044B\u0439 \u043D\u0430 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0438 \u0438 \`store\` (\u043E\u043D \u0436\u0435 \`ctx\`
582
+ * \u0445\u0443\u043A\u043E\u0432). \u041E\u0434\u0438\u043D \u0438 \u0442\u043E\u0442 \u0436\u0435 \u0434\u0432\u0438\u0436\u043E\u043A \u0438 \u0434\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u0438 \u0434\u043B\u044F world.
583
+ */
584
+ private makeRequest(cfg: {
585
+ headers: Record<string, string>
586
+ store: BaseStore
587
+ }): RequestFn {
588
+ const baseUrl = this.baseUrl()
589
+ const registry = this.hookRegistry
590
+ const { headers: baseHeaders, store } = cfg
591
+
592
+ return async (req): Promise<${tail.returnType}> => {
593
+ const { method, path, opKey, input } = req
594
+
595
+ const hookReq = {
596
+ method,
597
+ path,
598
+ pathParams: { ...(input?.path ?? {}) },
599
+ query: { ...(input?.query ?? {}) },
600
+ body: input?.body,
601
+ headers: { ...baseHeaders },
602
+ }
603
+ await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
604
+
605
+ let resolvedPath = path
606
+ for (const [k, v] of Object.entries(hookReq.pathParams)) {
607
+ resolvedPath = resolvedPath.replace(
608
+ \`{\${k}}\`,
609
+ encodeURIComponent(String(v)),
610
+ )
611
+ }
612
+
613
+ const url = new URL(baseUrl + resolvedPath)
614
+ for (const [k, v] of Object.entries(hookReq.query)) {
615
+ if (v !== undefined) url.searchParams.set(k, String(v))
616
+ }
617
+
618
+ const hasBody = hookReq.body !== undefined
619
+ const response = await fetch(url, {
620
+ method,
621
+ headers: {
622
+ ...(hasBody ? { 'content-type': 'application/json' } : {}),
623
+ ...hookReq.headers,
624
+ },
625
+ body: hasBody ? JSON.stringify(hookReq.body) : undefined,
626
+ })
627
+
628
+ const text = await response.text()
629
+ let data: unknown
630
+ try {
631
+ data = text ? JSON.parse(text) : undefined
632
+ } catch {
633
+ data = text
634
+ }
635
+
636
+ await registry.runAfter({
637
+ key: opKey,
638
+ request: hookReq,
639
+ response: { status: response.status, body: data },
640
+ ctx: store,
641
+ })
642
+
643
+ ${tail.ret}
644
+ }
645
+ }
646
+
647
+ ${renderApiTreeBuilder({ groups, mode })}
648
+
649
+ /**
650
+ * \u0421\u043E\u0437\u0434\u0430\u0451\u0442 \xAB\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\xBB \u2014 \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u0442\u0438\u043F\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0432\u044B\u0437\u043E\u0432\u043E\u0432
651
+ * \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043F\u043E\u0434 \u043A\u0430\u043F\u043E\u0442\u043E\u043C \u0445\u043E\u0434\u0438\u0442 fetch-\u0437\u0430\u043F\u0440\u043E\u0441\u0430\u043C\u0438. \`store.api\`
652
+ * \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043D\u0430 \u0442\u043E \u0436\u0435 \u0434\u0435\u0440\u0435\u0432\u043E, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043C\u0435\u0442\u043E\u0434\u044B-\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438 \u044D\u0442\u043E\u0433\u043E store
653
+ * (\`fullRegister\` \u0438 \u0442.\u043F.) \u0445\u043E\u0434\u044F\u0442 \u043E\u0442 \u043B\u0438\u0446\u0430 \u0438\u043C\u0435\u043D\u043D\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F.
654
+ */
655
+ createUser(userArgs: CreateUserArgs = {}) {
656
+ const StoreClass =
657
+ this.args.store ?? (ebely.userStore as unknown as new () => Store)
658
+ const store = new StoreClass()
659
+ const tree = this.buildApiTree(
660
+ this.makeRequest({ headers: { ...userArgs.headers }, store }),
661
+ )
662
+ ;(store as unknown as { api: unknown }).api = tree
663
+ return Object.assign(store, tree)
664
+ }
665
+ }
666
+ `;
667
+ }
668
+
669
+ // src/generator/swagger.ts
670
+ var import_promises = require("fs/promises");
671
+ var import_node_path = require("path");
672
+ async function loadSpec(args) {
673
+ const { swagger } = args;
674
+ if (swagger.pathToFile !== void 0) {
675
+ const specPath = (0, import_node_path.resolve)(process.cwd(), swagger.pathToFile);
676
+ return JSON.parse(await (0, import_promises.readFile)(specPath, "utf8"));
677
+ }
678
+ const response = await fetch(swagger.url);
679
+ if (!response.ok) {
680
+ throw new Error(
681
+ `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C swagger \u043F\u043E ${swagger.url}: ${response.status} ${response.statusText}`
682
+ );
683
+ }
684
+ return await response.json();
685
+ }
686
+
687
+ // src/generate-client.ts
688
+ async function generateClient(args) {
689
+ const {
690
+ swagger,
691
+ generateClientTo,
692
+ mode = "test",
693
+ userStoreImport = "ebely",
694
+ configImport = "./ebely"
695
+ } = args;
696
+ const outPath = (0, import_node_path2.resolve)(process.cwd(), generateClientTo);
697
+ const spec = await loadSpec({ swagger });
698
+ const operations = collectOperations({ spec });
699
+ const source = renderClient({
700
+ spec,
701
+ operations,
702
+ mode,
703
+ userStoreImport,
704
+ configImport
705
+ });
706
+ await (0, import_promises2.writeFile)(outPath, source, "utf8");
707
+ console.log(`
708
+ \u2705 Typed client written to:
709
+ ${outPath}
710
+
711
+ Endpoints: ${operations.length}
712
+ `);
713
+ return { outPath, operations: operations.length };
714
+ }
715
+ // Annotate the CommonJS export names for ESM import in node:
716
+ 0 && (module.exports = {
717
+ ApiResponse,
718
+ BaseStore,
719
+ EbelyAssertionError,
720
+ HookRegistry,
721
+ generateClient
722
+ });