ebely 0.0.5 → 0.0.7
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.d.ts +67 -3
- package/dist/index.js +166 -53
- package/dist/index.mjs +166 -53
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,24 @@ type AfterHookArgs<Ctx, Body = unknown, ResBody = unknown> = {
|
|
|
110
110
|
type BeforeHook<Ctx, Body = unknown> = (args: BeforeHookArgs<Ctx, Body>) => void | Promise<void>;
|
|
111
111
|
/** `after`-хук: вызывается ПОСЛЕ ответа; `response` — только на чтение. */
|
|
112
112
|
type AfterHook<Ctx, Body = unknown, ResBody = unknown> = (args: AfterHookArgs<Ctx, Body, ResBody>) => void | Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* `retry`-хук: вызывается ПОСЛЕ ответа (и после всех `after`-хуков) и
|
|
115
|
+
* РЕШАЕТ, нужно ли переиграть тот же запрос. Возврат `true` → ebely
|
|
116
|
+
* выполняет запрос заново (с нуля: снова прогоняет `before`-хуки и шлёт
|
|
117
|
+
* fetch), поэтому правки, сделанные внутри хука (напр. свежий токен в
|
|
118
|
+
* `ctx`), автоматически подхватятся повторным `before`. Любой
|
|
119
|
+
* не-истинный возврат (`false` / `undefined`) → ретрая нет, отдаём ответ
|
|
120
|
+
* как есть.
|
|
121
|
+
*
|
|
122
|
+
* Аргумент — тот же, что у `after` (`request` / `response` / `ctx`):
|
|
123
|
+
* `response` тут на чтение (его смотрят, чтобы решить про ретрай), а
|
|
124
|
+
* подготовку к повтору (refresh токена и т.п.) хук делает через `ctx`.
|
|
125
|
+
*
|
|
126
|
+
* Число повторов ограничено ядром (`EbelyConfig.maxRetries`, по
|
|
127
|
+
* умолчанию 3) — даже если хук упрямо возвращает `true`, бесконечного
|
|
128
|
+
* цикла не будет.
|
|
129
|
+
*/
|
|
130
|
+
type RetryHook<Ctx, Body = unknown, ResBody = unknown> = (args: AfterHookArgs<Ctx, Body, ResBody>) => boolean | void | Promise<boolean | void>;
|
|
113
131
|
/**
|
|
114
132
|
* Регистратор хуков, который пользователь передаёт в конфиг одной
|
|
115
133
|
* переменной (`EbelyConfig.hooks`). Точную типизированную форму аргумента
|
|
@@ -137,6 +155,9 @@ type HooksRegistrar = (registrar: any) => void;
|
|
|
137
155
|
declare class HookRegistry {
|
|
138
156
|
private beforeMap;
|
|
139
157
|
private afterMap;
|
|
158
|
+
private globalBeforeHooks;
|
|
159
|
+
private globalAfterHooks;
|
|
160
|
+
private globalRetryHooks;
|
|
140
161
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
141
162
|
before(args: {
|
|
142
163
|
key: string;
|
|
@@ -147,19 +168,52 @@ declare class HookRegistry {
|
|
|
147
168
|
key: string;
|
|
148
169
|
fn: AfterHook<any>;
|
|
149
170
|
}): void;
|
|
150
|
-
/**
|
|
171
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
172
|
+
globalBefore(args: {
|
|
173
|
+
fn: BeforeHook<any>;
|
|
174
|
+
}): void;
|
|
175
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
176
|
+
globalAfter(args: {
|
|
177
|
+
fn: AfterHook<any>;
|
|
178
|
+
}): void;
|
|
179
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
180
|
+
globalRetry(args: {
|
|
181
|
+
fn: RetryHook<any>;
|
|
182
|
+
}): void;
|
|
183
|
+
/**
|
|
184
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
185
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
186
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
187
|
+
*/
|
|
151
188
|
runBefore(args: {
|
|
152
189
|
key: string;
|
|
153
190
|
request: HookRequest;
|
|
154
191
|
ctx: unknown;
|
|
155
192
|
}): Promise<void>;
|
|
156
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
195
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
196
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
197
|
+
*/
|
|
157
198
|
runAfter(args: {
|
|
158
199
|
key: string;
|
|
159
200
|
request: HookRequest;
|
|
160
201
|
response: HookResponse;
|
|
161
202
|
ctx: unknown;
|
|
162
203
|
}): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
206
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
207
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
208
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
209
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
210
|
+
*/
|
|
211
|
+
runRetry(args: {
|
|
212
|
+
key: string;
|
|
213
|
+
request: HookRequest;
|
|
214
|
+
response: HookResponse;
|
|
215
|
+
ctx: unknown;
|
|
216
|
+
}): Promise<boolean>;
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
/**
|
|
@@ -218,6 +272,16 @@ type EbelyConfig = {
|
|
|
218
272
|
* @default 'test'
|
|
219
273
|
*/
|
|
220
274
|
mode?: ClientMode;
|
|
275
|
+
/**
|
|
276
|
+
* Максимальное число ПОВТОРОВ запроса, которые ebely сделает, если
|
|
277
|
+
* глобальный `retry`-хук (`h.globalRetry`) вернул `true`. Это потолок
|
|
278
|
+
* на повторы СВЕРХ первой попытки: при `maxRetries: 3` запрос уйдёт
|
|
279
|
+
* максимум 4 раза. Защита от бесконечного цикла, если хук упрямо просит
|
|
280
|
+
* повтор (напр. сервер стабильно отвечает 401). Без `globalRetry`-хуков
|
|
281
|
+
* не влияет ни на что.
|
|
282
|
+
* @default 3
|
|
283
|
+
*/
|
|
284
|
+
maxRetries?: number;
|
|
221
285
|
/**
|
|
222
286
|
* Регистратор хуков `before` / `after`. Объявляется в ОТДЕЛЬНОМ
|
|
223
287
|
* типизированном файле (тип `Hooks` экспортирует сгенерированный
|
|
@@ -270,4 +334,4 @@ declare function generateClient(args: EbelyConfig): Promise<{
|
|
|
270
334
|
operations: number;
|
|
271
335
|
}>;
|
|
272
336
|
|
|
273
|
-
export { AfterHook, AfterHookArgs, ApiResponse, BaseStore, BeforeHook, BeforeHookArgs, ClientMode, DeepPartial, EbelyAssertionError, EbelyConfig, HookRegistry, HookRequest, HookResponse, HooksRegistrar, SwaggerSource, generateClient };
|
|
337
|
+
export { AfterHook, AfterHookArgs, ApiResponse, BaseStore, BeforeHook, BeforeHookArgs, ClientMode, DeepPartial, EbelyAssertionError, EbelyConfig, HookRegistry, HookRequest, HookResponse, HooksRegistrar, RetryHook, SwaggerSource, generateClient };
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,15 @@ var ApiResponse = class {
|
|
|
163
163
|
var HookRegistry = class {
|
|
164
164
|
beforeMap = /* @__PURE__ */ new Map();
|
|
165
165
|
afterMap = /* @__PURE__ */ new Map();
|
|
166
|
+
// Глобальные хуки — срабатывают на КАЖДУЮ операцию (вне зависимости от
|
|
167
|
+
// ключа). Удобны для сквозных задач: подстановка `Authorization` из
|
|
168
|
+
// `ctx` во все запросы, логирование, обработка 401 и т.п.
|
|
169
|
+
globalBeforeHooks = [];
|
|
170
|
+
globalAfterHooks = [];
|
|
171
|
+
// Глобальные retry-хуки — решают, переигрывать ли запрос (напр. на 401
|
|
172
|
+
// сходить за refresh и вернуть `true`). Цикл повторов и его лимит живут
|
|
173
|
+
// в сгенерированном `request` (см. render.ts) — сам реестр сети не знает.
|
|
174
|
+
globalRetryHooks = [];
|
|
166
175
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
167
176
|
before(args) {
|
|
168
177
|
const list = this.beforeMap.get(args.key) ?? [];
|
|
@@ -175,18 +184,59 @@ var HookRegistry = class {
|
|
|
175
184
|
list.push(args.fn);
|
|
176
185
|
this.afterMap.set(args.key, list);
|
|
177
186
|
}
|
|
178
|
-
/**
|
|
187
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
188
|
+
globalBefore(args) {
|
|
189
|
+
this.globalBeforeHooks.push(args.fn);
|
|
190
|
+
}
|
|
191
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
192
|
+
globalAfter(args) {
|
|
193
|
+
this.globalAfterHooks.push(args.fn);
|
|
194
|
+
}
|
|
195
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
196
|
+
globalRetry(args) {
|
|
197
|
+
this.globalRetryHooks.push(args.fn);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
201
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
202
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
203
|
+
*/
|
|
179
204
|
async runBefore(args) {
|
|
180
|
-
|
|
205
|
+
const hooks = [...this.globalBeforeHooks, ...this.beforeMap.get(args.key) ?? []];
|
|
206
|
+
for (const fn of hooks) {
|
|
181
207
|
await fn({ request: args.request, ctx: args.ctx });
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
212
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
213
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
214
|
+
*/
|
|
185
215
|
async runAfter(args) {
|
|
186
|
-
|
|
216
|
+
const hooks = [...this.afterMap.get(args.key) ?? [], ...this.globalAfterHooks];
|
|
217
|
+
for (const fn of hooks) {
|
|
187
218
|
await fn({ request: args.request, response: args.response, ctx: args.ctx });
|
|
188
219
|
}
|
|
189
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
223
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
224
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
225
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
226
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
227
|
+
*/
|
|
228
|
+
async runRetry(args) {
|
|
229
|
+
for (const fn of this.globalRetryHooks) {
|
|
230
|
+
const decision = await fn({
|
|
231
|
+
request: args.request,
|
|
232
|
+
response: args.response,
|
|
233
|
+
ctx: args.ctx
|
|
234
|
+
});
|
|
235
|
+
if (decision)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
190
240
|
};
|
|
191
241
|
|
|
192
242
|
// src/generate-client.ts
|
|
@@ -314,8 +364,38 @@ function collectOperations(args) {
|
|
|
314
364
|
});
|
|
315
365
|
}
|
|
316
366
|
}
|
|
367
|
+
dedupeNames({ operations });
|
|
317
368
|
return operations;
|
|
318
369
|
}
|
|
370
|
+
function prefixMethod(args) {
|
|
371
|
+
const { method, name } = args;
|
|
372
|
+
return method + name.charAt(0).toUpperCase() + name.slice(1);
|
|
373
|
+
}
|
|
374
|
+
function dedupeNames(args) {
|
|
375
|
+
const { operations } = args;
|
|
376
|
+
const counts = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const op of operations) {
|
|
378
|
+
const byName = counts.get(op.group) ?? /* @__PURE__ */ new Map();
|
|
379
|
+
byName.set(op.name, (byName.get(op.name) ?? 0) + 1);
|
|
380
|
+
counts.set(op.group, byName);
|
|
381
|
+
}
|
|
382
|
+
const taken = /* @__PURE__ */ new Map();
|
|
383
|
+
const claim = (group, name) => {
|
|
384
|
+
const used = taken.get(group) ?? /* @__PURE__ */ new Set();
|
|
385
|
+
let candidate = name;
|
|
386
|
+
let i = 2;
|
|
387
|
+
while (used.has(candidate))
|
|
388
|
+
candidate = `${name}${i++}`;
|
|
389
|
+
used.add(candidate);
|
|
390
|
+
taken.set(group, used);
|
|
391
|
+
return candidate;
|
|
392
|
+
};
|
|
393
|
+
for (const op of operations) {
|
|
394
|
+
const collides = (counts.get(op.group)?.get(op.name) ?? 0) > 1;
|
|
395
|
+
const base = collides ? prefixMethod({ method: op.method, name: op.name }) : op.name;
|
|
396
|
+
op.name = claim(op.group, base);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
319
399
|
|
|
320
400
|
// src/generator/render.ts
|
|
321
401
|
function buildInputType(op) {
|
|
@@ -380,7 +460,7 @@ function renderImports(args) {
|
|
|
380
460
|
const { mode, userStoreImport, configImport } = args;
|
|
381
461
|
const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
|
|
382
462
|
return `import { ${values} } from ${JSON.stringify(userStoreImport)}
|
|
383
|
-
import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
|
|
463
|
+
import type { BeforeHook, AfterHook, RetryHook } from ${JSON.stringify(userStoreImport)}
|
|
384
464
|
import { ebely } from ${JSON.stringify(configImport)}`;
|
|
385
465
|
}
|
|
386
466
|
function renderRequestTail(mode) {
|
|
@@ -439,7 +519,11 @@ function renderHookTreeType(groups) {
|
|
|
439
519
|
${leaves.join("\n")}
|
|
440
520
|
}`;
|
|
441
521
|
});
|
|
522
|
+
const global = ` globalBefore(fn: BeforeHook<Store>): void
|
|
523
|
+
globalAfter(fn: AfterHook<Store>): void
|
|
524
|
+
globalRetry(fn: RetryHook<Store>): void`;
|
|
442
525
|
return `type EbelyHookTree<Store extends BaseStore> = {
|
|
526
|
+
${global}
|
|
443
527
|
${blocks.join("\n")}
|
|
444
528
|
}`;
|
|
445
529
|
}
|
|
@@ -459,6 +543,9 @@ ${leaves.join("\n")}
|
|
|
459
543
|
return ` private buildHookTree(): EbelyHookTree<Store> {
|
|
460
544
|
const r = this.hookRegistry
|
|
461
545
|
return {
|
|
546
|
+
globalBefore: (fn: BeforeHook<Store>) => r.globalBefore({ fn }),
|
|
547
|
+
globalAfter: (fn: AfterHook<Store>) => r.globalAfter({ fn }),
|
|
548
|
+
globalRetry: (fn: RetryHook<Store>) => r.globalRetry({ fn }),
|
|
462
549
|
${blocks.join("\n")}
|
|
463
550
|
} as unknown as EbelyHookTree<Store>
|
|
464
551
|
}`;
|
|
@@ -483,6 +570,7 @@ function renderClient(args) {
|
|
|
483
570
|
const basePath = spec.servers?.[0]?.url ?? "";
|
|
484
571
|
const groups = groupOperations(operations);
|
|
485
572
|
const tail = renderRequestTail(mode);
|
|
573
|
+
const tailRet = tail.ret.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n");
|
|
486
574
|
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
575
|
// \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
|
|
488
576
|
// \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
|
|
@@ -588,59 +676,84 @@ ${renderHookTreeBuilder(groups)}
|
|
|
588
676
|
const baseUrl = this.baseUrl()
|
|
589
677
|
const registry = this.hookRegistry
|
|
590
678
|
const { headers: baseHeaders, store } = cfg
|
|
679
|
+
// \u041F\u043E\u0442\u043E\u043B\u043E\u043A \u041F\u041E\u0412\u0422\u041E\u0420\u041E\u0412 (\u0441\u0432\u0435\u0440\u0445 \u043F\u0435\u0440\u0432\u043E\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438) \u0434\u043B\u044F globalRetry-\u0445\u0443\u043A\u043E\u0432 \u2014
|
|
680
|
+
// \u0437\u0430\u0449\u0438\u0442\u0430 \u043E\u0442 \u0431\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E\u0433\u043E \u0446\u0438\u043A\u043B\u0430, \u0435\u0441\u043B\u0438 \u0445\u0443\u043A \u0443\u043F\u0440\u044F\u043C\u043E \u043F\u0440\u043E\u0441\u0438\u0442 \u043F\u043E\u0432\u0442\u043E\u0440.
|
|
681
|
+
const maxRetries = (ebely as { maxRetries?: number }).maxRetries ?? 3
|
|
591
682
|
|
|
592
683
|
return async (req): Promise<${tail.returnType}> => {
|
|
593
684
|
const { method, path, opKey, input } = req
|
|
594
685
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
686
|
+
// Retry-\u0446\u0438\u043A\u043B: \u043A\u0430\u0436\u0434\u0443\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443 hookReq \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0417\u0410\u041D\u041E\u0412\u041E \u0438 \u0437\u0430\u043D\u043E\u0432\u043E
|
|
687
|
+
// \u043F\u0440\u043E\u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F before-\u0445\u0443\u043A\u0438, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u0440\u0430\u0432\u043A\u0438 \u0438\u0437 retry-\u0445\u0443\u043A\u0430 (\u043D\u0430\u043F\u0440.
|
|
688
|
+
// \u0441\u0432\u0435\u0436\u0438\u0439 \u0442\u043E\u043A\u0435\u043D \u0432 ctx) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u043C before. \`for (;;)\`
|
|
689
|
+
// \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F, \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 return/throw \u0432 \u0445\u0432\u043E\u0441\u0442\u0435 \u0438\u043B\u0438 \`continue\`.
|
|
690
|
+
let attempt = 0
|
|
691
|
+
for (;;) {
|
|
692
|
+
const hookReq = {
|
|
693
|
+
method,
|
|
694
|
+
path,
|
|
695
|
+
pathParams: { ...(input?.path ?? {}) },
|
|
696
|
+
query: { ...(input?.query ?? {}) },
|
|
697
|
+
body: input?.body,
|
|
698
|
+
headers: { ...baseHeaders },
|
|
699
|
+
}
|
|
700
|
+
await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
|
|
701
|
+
|
|
702
|
+
let resolvedPath = path
|
|
703
|
+
for (const [k, v] of Object.entries(hookReq.pathParams)) {
|
|
704
|
+
resolvedPath = resolvedPath.replace(
|
|
705
|
+
\`{\${k}}\`,
|
|
706
|
+
encodeURIComponent(String(v)),
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const url = new URL(baseUrl + resolvedPath)
|
|
711
|
+
for (const [k, v] of Object.entries(hookReq.query)) {
|
|
712
|
+
if (v !== undefined) url.searchParams.set(k, String(v))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const hasBody = hookReq.body !== undefined
|
|
716
|
+
const response = await fetch(url, {
|
|
717
|
+
method,
|
|
718
|
+
headers: {
|
|
719
|
+
...(hasBody ? { 'content-type': 'application/json' } : {}),
|
|
720
|
+
...hookReq.headers,
|
|
721
|
+
},
|
|
722
|
+
body: hasBody ? JSON.stringify(hookReq.body) : undefined,
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
const text = await response.text()
|
|
726
|
+
let data: unknown
|
|
727
|
+
try {
|
|
728
|
+
data = text ? JSON.parse(text) : undefined
|
|
729
|
+
} catch {
|
|
730
|
+
data = text
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
await registry.runAfter({
|
|
734
|
+
key: opKey,
|
|
735
|
+
request: hookReq,
|
|
736
|
+
response: { status: response.status, body: data },
|
|
737
|
+
ctx: store,
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
// \u041F\u043E\u043A\u0430 \u0435\u0441\u0442\u044C \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432 \u2014 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C retry-\u0445\u0443\u043A\u0438. \u0412\u0435\u0440\u043D\u0443\u043B\u0438
|
|
741
|
+
// true \u2192 \u043D\u043E\u0432\u044B\u0439 \u0432\u0438\u0442\u043E\u043A (\u0437\u0430\u043D\u043E\u0432\u043E before \u2192 fetch); \u0438\u043D\u0430\u0447\u0435 \u043E\u0442\u0434\u0430\u0451\u043C \u043E\u0442\u0432\u0435\u0442.
|
|
742
|
+
if (attempt < maxRetries) {
|
|
743
|
+
const shouldRetry = await registry.runRetry({
|
|
744
|
+
key: opKey,
|
|
745
|
+
request: hookReq,
|
|
746
|
+
response: { status: response.status, body: data },
|
|
747
|
+
ctx: store,
|
|
748
|
+
})
|
|
749
|
+
if (shouldRetry) {
|
|
750
|
+
attempt++
|
|
751
|
+
continue
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
${tailRet}
|
|
611
756
|
}
|
|
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
757
|
}
|
|
645
758
|
}
|
|
646
759
|
|
package/dist/index.mjs
CHANGED
|
@@ -133,6 +133,15 @@ var ApiResponse = class {
|
|
|
133
133
|
var HookRegistry = class {
|
|
134
134
|
beforeMap = /* @__PURE__ */ new Map();
|
|
135
135
|
afterMap = /* @__PURE__ */ new Map();
|
|
136
|
+
// Глобальные хуки — срабатывают на КАЖДУЮ операцию (вне зависимости от
|
|
137
|
+
// ключа). Удобны для сквозных задач: подстановка `Authorization` из
|
|
138
|
+
// `ctx` во все запросы, логирование, обработка 401 и т.п.
|
|
139
|
+
globalBeforeHooks = [];
|
|
140
|
+
globalAfterHooks = [];
|
|
141
|
+
// Глобальные retry-хуки — решают, переигрывать ли запрос (напр. на 401
|
|
142
|
+
// сходить за refresh и вернуть `true`). Цикл повторов и его лимит живут
|
|
143
|
+
// в сгенерированном `request` (см. render.ts) — сам реестр сети не знает.
|
|
144
|
+
globalRetryHooks = [];
|
|
136
145
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
137
146
|
before(args) {
|
|
138
147
|
const list = this.beforeMap.get(args.key) ?? [];
|
|
@@ -145,18 +154,59 @@ var HookRegistry = class {
|
|
|
145
154
|
list.push(args.fn);
|
|
146
155
|
this.afterMap.set(args.key, list);
|
|
147
156
|
}
|
|
148
|
-
/**
|
|
157
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
158
|
+
globalBefore(args) {
|
|
159
|
+
this.globalBeforeHooks.push(args.fn);
|
|
160
|
+
}
|
|
161
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
162
|
+
globalAfter(args) {
|
|
163
|
+
this.globalAfterHooks.push(args.fn);
|
|
164
|
+
}
|
|
165
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
166
|
+
globalRetry(args) {
|
|
167
|
+
this.globalRetryHooks.push(args.fn);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
171
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
172
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
173
|
+
*/
|
|
149
174
|
async runBefore(args) {
|
|
150
|
-
|
|
175
|
+
const hooks = [...this.globalBeforeHooks, ...this.beforeMap.get(args.key) ?? []];
|
|
176
|
+
for (const fn of hooks) {
|
|
151
177
|
await fn({ request: args.request, ctx: args.ctx });
|
|
152
178
|
}
|
|
153
179
|
}
|
|
154
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
182
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
183
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
184
|
+
*/
|
|
155
185
|
async runAfter(args) {
|
|
156
|
-
|
|
186
|
+
const hooks = [...this.afterMap.get(args.key) ?? [], ...this.globalAfterHooks];
|
|
187
|
+
for (const fn of hooks) {
|
|
157
188
|
await fn({ request: args.request, response: args.response, ctx: args.ctx });
|
|
158
189
|
}
|
|
159
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
193
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
194
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
195
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
196
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
197
|
+
*/
|
|
198
|
+
async runRetry(args) {
|
|
199
|
+
for (const fn of this.globalRetryHooks) {
|
|
200
|
+
const decision = await fn({
|
|
201
|
+
request: args.request,
|
|
202
|
+
response: args.response,
|
|
203
|
+
ctx: args.ctx
|
|
204
|
+
});
|
|
205
|
+
if (decision)
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
160
210
|
};
|
|
161
211
|
|
|
162
212
|
// src/generate-client.ts
|
|
@@ -284,8 +334,38 @@ function collectOperations(args) {
|
|
|
284
334
|
});
|
|
285
335
|
}
|
|
286
336
|
}
|
|
337
|
+
dedupeNames({ operations });
|
|
287
338
|
return operations;
|
|
288
339
|
}
|
|
340
|
+
function prefixMethod(args) {
|
|
341
|
+
const { method, name } = args;
|
|
342
|
+
return method + name.charAt(0).toUpperCase() + name.slice(1);
|
|
343
|
+
}
|
|
344
|
+
function dedupeNames(args) {
|
|
345
|
+
const { operations } = args;
|
|
346
|
+
const counts = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const op of operations) {
|
|
348
|
+
const byName = counts.get(op.group) ?? /* @__PURE__ */ new Map();
|
|
349
|
+
byName.set(op.name, (byName.get(op.name) ?? 0) + 1);
|
|
350
|
+
counts.set(op.group, byName);
|
|
351
|
+
}
|
|
352
|
+
const taken = /* @__PURE__ */ new Map();
|
|
353
|
+
const claim = (group, name) => {
|
|
354
|
+
const used = taken.get(group) ?? /* @__PURE__ */ new Set();
|
|
355
|
+
let candidate = name;
|
|
356
|
+
let i = 2;
|
|
357
|
+
while (used.has(candidate))
|
|
358
|
+
candidate = `${name}${i++}`;
|
|
359
|
+
used.add(candidate);
|
|
360
|
+
taken.set(group, used);
|
|
361
|
+
return candidate;
|
|
362
|
+
};
|
|
363
|
+
for (const op of operations) {
|
|
364
|
+
const collides = (counts.get(op.group)?.get(op.name) ?? 0) > 1;
|
|
365
|
+
const base = collides ? prefixMethod({ method: op.method, name: op.name }) : op.name;
|
|
366
|
+
op.name = claim(op.group, base);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
289
369
|
|
|
290
370
|
// src/generator/render.ts
|
|
291
371
|
function buildInputType(op) {
|
|
@@ -350,7 +430,7 @@ function renderImports(args) {
|
|
|
350
430
|
const { mode, userStoreImport, configImport } = args;
|
|
351
431
|
const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
|
|
352
432
|
return `import { ${values} } from ${JSON.stringify(userStoreImport)}
|
|
353
|
-
import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
|
|
433
|
+
import type { BeforeHook, AfterHook, RetryHook } from ${JSON.stringify(userStoreImport)}
|
|
354
434
|
import { ebely } from ${JSON.stringify(configImport)}`;
|
|
355
435
|
}
|
|
356
436
|
function renderRequestTail(mode) {
|
|
@@ -409,7 +489,11 @@ function renderHookTreeType(groups) {
|
|
|
409
489
|
${leaves.join("\n")}
|
|
410
490
|
}`;
|
|
411
491
|
});
|
|
492
|
+
const global = ` globalBefore(fn: BeforeHook<Store>): void
|
|
493
|
+
globalAfter(fn: AfterHook<Store>): void
|
|
494
|
+
globalRetry(fn: RetryHook<Store>): void`;
|
|
412
495
|
return `type EbelyHookTree<Store extends BaseStore> = {
|
|
496
|
+
${global}
|
|
413
497
|
${blocks.join("\n")}
|
|
414
498
|
}`;
|
|
415
499
|
}
|
|
@@ -429,6 +513,9 @@ ${leaves.join("\n")}
|
|
|
429
513
|
return ` private buildHookTree(): EbelyHookTree<Store> {
|
|
430
514
|
const r = this.hookRegistry
|
|
431
515
|
return {
|
|
516
|
+
globalBefore: (fn: BeforeHook<Store>) => r.globalBefore({ fn }),
|
|
517
|
+
globalAfter: (fn: AfterHook<Store>) => r.globalAfter({ fn }),
|
|
518
|
+
globalRetry: (fn: RetryHook<Store>) => r.globalRetry({ fn }),
|
|
432
519
|
${blocks.join("\n")}
|
|
433
520
|
} as unknown as EbelyHookTree<Store>
|
|
434
521
|
}`;
|
|
@@ -453,6 +540,7 @@ function renderClient(args) {
|
|
|
453
540
|
const basePath = spec.servers?.[0]?.url ?? "";
|
|
454
541
|
const groups = groupOperations(operations);
|
|
455
542
|
const tail = renderRequestTail(mode);
|
|
543
|
+
const tailRet = tail.ret.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n");
|
|
456
544
|
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.
|
|
457
545
|
// \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
|
|
458
546
|
// \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
|
|
@@ -558,59 +646,84 @@ ${renderHookTreeBuilder(groups)}
|
|
|
558
646
|
const baseUrl = this.baseUrl()
|
|
559
647
|
const registry = this.hookRegistry
|
|
560
648
|
const { headers: baseHeaders, store } = cfg
|
|
649
|
+
// \u041F\u043E\u0442\u043E\u043B\u043E\u043A \u041F\u041E\u0412\u0422\u041E\u0420\u041E\u0412 (\u0441\u0432\u0435\u0440\u0445 \u043F\u0435\u0440\u0432\u043E\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438) \u0434\u043B\u044F globalRetry-\u0445\u0443\u043A\u043E\u0432 \u2014
|
|
650
|
+
// \u0437\u0430\u0449\u0438\u0442\u0430 \u043E\u0442 \u0431\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E\u0433\u043E \u0446\u0438\u043A\u043B\u0430, \u0435\u0441\u043B\u0438 \u0445\u0443\u043A \u0443\u043F\u0440\u044F\u043C\u043E \u043F\u0440\u043E\u0441\u0438\u0442 \u043F\u043E\u0432\u0442\u043E\u0440.
|
|
651
|
+
const maxRetries = (ebely as { maxRetries?: number }).maxRetries ?? 3
|
|
561
652
|
|
|
562
653
|
return async (req): Promise<${tail.returnType}> => {
|
|
563
654
|
const { method, path, opKey, input } = req
|
|
564
655
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
656
|
+
// Retry-\u0446\u0438\u043A\u043B: \u043A\u0430\u0436\u0434\u0443\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443 hookReq \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0417\u0410\u041D\u041E\u0412\u041E \u0438 \u0437\u0430\u043D\u043E\u0432\u043E
|
|
657
|
+
// \u043F\u0440\u043E\u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F before-\u0445\u0443\u043A\u0438, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u0440\u0430\u0432\u043A\u0438 \u0438\u0437 retry-\u0445\u0443\u043A\u0430 (\u043D\u0430\u043F\u0440.
|
|
658
|
+
// \u0441\u0432\u0435\u0436\u0438\u0439 \u0442\u043E\u043A\u0435\u043D \u0432 ctx) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u043C before. \`for (;;)\`
|
|
659
|
+
// \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F, \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 return/throw \u0432 \u0445\u0432\u043E\u0441\u0442\u0435 \u0438\u043B\u0438 \`continue\`.
|
|
660
|
+
let attempt = 0
|
|
661
|
+
for (;;) {
|
|
662
|
+
const hookReq = {
|
|
663
|
+
method,
|
|
664
|
+
path,
|
|
665
|
+
pathParams: { ...(input?.path ?? {}) },
|
|
666
|
+
query: { ...(input?.query ?? {}) },
|
|
667
|
+
body: input?.body,
|
|
668
|
+
headers: { ...baseHeaders },
|
|
669
|
+
}
|
|
670
|
+
await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
|
|
671
|
+
|
|
672
|
+
let resolvedPath = path
|
|
673
|
+
for (const [k, v] of Object.entries(hookReq.pathParams)) {
|
|
674
|
+
resolvedPath = resolvedPath.replace(
|
|
675
|
+
\`{\${k}}\`,
|
|
676
|
+
encodeURIComponent(String(v)),
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const url = new URL(baseUrl + resolvedPath)
|
|
681
|
+
for (const [k, v] of Object.entries(hookReq.query)) {
|
|
682
|
+
if (v !== undefined) url.searchParams.set(k, String(v))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const hasBody = hookReq.body !== undefined
|
|
686
|
+
const response = await fetch(url, {
|
|
687
|
+
method,
|
|
688
|
+
headers: {
|
|
689
|
+
...(hasBody ? { 'content-type': 'application/json' } : {}),
|
|
690
|
+
...hookReq.headers,
|
|
691
|
+
},
|
|
692
|
+
body: hasBody ? JSON.stringify(hookReq.body) : undefined,
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
const text = await response.text()
|
|
696
|
+
let data: unknown
|
|
697
|
+
try {
|
|
698
|
+
data = text ? JSON.parse(text) : undefined
|
|
699
|
+
} catch {
|
|
700
|
+
data = text
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
await registry.runAfter({
|
|
704
|
+
key: opKey,
|
|
705
|
+
request: hookReq,
|
|
706
|
+
response: { status: response.status, body: data },
|
|
707
|
+
ctx: store,
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
// \u041F\u043E\u043A\u0430 \u0435\u0441\u0442\u044C \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432 \u2014 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C retry-\u0445\u0443\u043A\u0438. \u0412\u0435\u0440\u043D\u0443\u043B\u0438
|
|
711
|
+
// true \u2192 \u043D\u043E\u0432\u044B\u0439 \u0432\u0438\u0442\u043E\u043A (\u0437\u0430\u043D\u043E\u0432\u043E before \u2192 fetch); \u0438\u043D\u0430\u0447\u0435 \u043E\u0442\u0434\u0430\u0451\u043C \u043E\u0442\u0432\u0435\u0442.
|
|
712
|
+
if (attempt < maxRetries) {
|
|
713
|
+
const shouldRetry = await registry.runRetry({
|
|
714
|
+
key: opKey,
|
|
715
|
+
request: hookReq,
|
|
716
|
+
response: { status: response.status, body: data },
|
|
717
|
+
ctx: store,
|
|
718
|
+
})
|
|
719
|
+
if (shouldRetry) {
|
|
720
|
+
attempt++
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
${tailRet}
|
|
581
726
|
}
|
|
582
|
-
|
|
583
|
-
const url = new URL(baseUrl + resolvedPath)
|
|
584
|
-
for (const [k, v] of Object.entries(hookReq.query)) {
|
|
585
|
-
if (v !== undefined) url.searchParams.set(k, String(v))
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const hasBody = hookReq.body !== undefined
|
|
589
|
-
const response = await fetch(url, {
|
|
590
|
-
method,
|
|
591
|
-
headers: {
|
|
592
|
-
...(hasBody ? { 'content-type': 'application/json' } : {}),
|
|
593
|
-
...hookReq.headers,
|
|
594
|
-
},
|
|
595
|
-
body: hasBody ? JSON.stringify(hookReq.body) : undefined,
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
const text = await response.text()
|
|
599
|
-
let data: unknown
|
|
600
|
-
try {
|
|
601
|
-
data = text ? JSON.parse(text) : undefined
|
|
602
|
-
} catch {
|
|
603
|
-
data = text
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
await registry.runAfter({
|
|
607
|
-
key: opKey,
|
|
608
|
-
request: hookReq,
|
|
609
|
-
response: { status: response.status, body: data },
|
|
610
|
-
ctx: store,
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
${tail.ret}
|
|
614
727
|
}
|
|
615
728
|
}
|
|
616
729
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebely",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
"build": "tsup index.ts --format cjs,esm --dts",
|
|
36
36
|
"release": "pnpm run build && changeset publish",
|
|
37
37
|
"lint": "tsc",
|
|
38
|
-
"test": "tsx --test src/response.test.ts src/hooks.test.ts src/generator/render.test.ts"
|
|
38
|
+
"test": "tsx --test src/response.test.ts src/hooks.test.ts src/generator/render.test.ts src/generator/operations.test.ts"
|
|
39
39
|
}
|
|
40
40
|
}
|