ebely 0.0.2 → 0.0.4

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.
@@ -0,0 +1,552 @@
1
+ // AUTO-GENERATED by ebely (generateClient) — НЕ редактировать вручную.
2
+ // Источник: Test Backend (oRPC) v1.0.0
3
+ // Режим клиента: test
4
+ // Перегенерация: pnpm run client:generate
5
+
6
+ import { BaseStore, ApiResponse, HookRegistry } from "ebely"
7
+ import type { BeforeHook, AfterHook } from "ebely"
8
+ import { ebely } from "./ebely"
9
+
10
+ type RequestInput = {
11
+ path?: Record<string, string>
12
+ query?: Record<string, string | number | boolean | undefined>
13
+ body?: unknown
14
+ }
15
+
16
+ type RequestFn = (req: {
17
+ method: string
18
+ path: string
19
+ opKey: string
20
+ input?: RequestInput
21
+ }) => Promise<{ status: number; body: unknown }>
22
+
23
+ export type CreateUserArgs = {
24
+ /** Заголовки, которые будут добавляться ко всем запросам этого пользователя. */
25
+ headers?: Record<string, string>
26
+ }
27
+
28
+ export type WorldApi = {
29
+ "posts": {
30
+ "list": (input?: {}) => Promise<ApiResponse<{ 200: Array<{
31
+ "id": string
32
+ "title": string
33
+ "content": string
34
+ "createdAt": string
35
+ "updatedAt": string
36
+ }> }>>
37
+ "create": (input: { body: {
38
+ "title": string
39
+ "content": string
40
+ } }) => Promise<ApiResponse<{ 200: {
41
+ "id": string
42
+ "title": string
43
+ "content": string
44
+ "createdAt": string
45
+ "updatedAt": string
46
+ } }>>
47
+ "get": (input: { path: { "id": string } }) => Promise<ApiResponse<{ 200: {
48
+ "id": string
49
+ "title": string
50
+ "content": string
51
+ "createdAt": string
52
+ "updatedAt": string
53
+ } }>>
54
+ "update": (input: { path: { "id": string }; body?: {
55
+ "title"?: string
56
+ "content"?: string
57
+ } }) => Promise<ApiResponse<{ 200: {
58
+ "id": string
59
+ "title": string
60
+ "content": string
61
+ "createdAt": string
62
+ "updatedAt": string
63
+ } }>>
64
+ "delete": (input: { path: { "id": string } }) => Promise<ApiResponse<{ 200: {
65
+ "success": boolean
66
+ } }>>
67
+ }
68
+ "auth": {
69
+ "register": (input: { body: {
70
+ "email": string
71
+ "password": string
72
+ } }) => Promise<ApiResponse<{ 200: {
73
+ "email": string
74
+ "password": string
75
+ } }>>
76
+ "confirm": (input: { body: {
77
+ "code": string
78
+ } }) => Promise<ApiResponse<{ 200: {
79
+ "code": string
80
+ } }>>
81
+ }
82
+ "admin": {
83
+ "clearDatabase": (input?: {}) => Promise<ApiResponse<{ 200: {
84
+ "success": boolean
85
+ } }>>
86
+ }
87
+ }
88
+
89
+ type EbelyHookTree<Store extends BaseStore> = {
90
+ "posts": {
91
+ "list": {
92
+ before(fn: BeforeHook<Store, undefined>): void
93
+ after(fn: AfterHook<Store, undefined, Array<{
94
+ "id": string
95
+ "title": string
96
+ "content": string
97
+ "createdAt": string
98
+ "updatedAt": string
99
+ }>>): void
100
+ }
101
+ "create": {
102
+ before(fn: BeforeHook<Store, {
103
+ "title": string
104
+ "content": string
105
+ }>): void
106
+ after(fn: AfterHook<Store, {
107
+ "title": string
108
+ "content": string
109
+ }, {
110
+ "id": string
111
+ "title": string
112
+ "content": string
113
+ "createdAt": string
114
+ "updatedAt": string
115
+ }>): void
116
+ }
117
+ "get": {
118
+ before(fn: BeforeHook<Store, undefined>): void
119
+ after(fn: AfterHook<Store, undefined, {
120
+ "id": string
121
+ "title": string
122
+ "content": string
123
+ "createdAt": string
124
+ "updatedAt": string
125
+ }>): void
126
+ }
127
+ "update": {
128
+ before(fn: BeforeHook<Store, {
129
+ "title"?: string
130
+ "content"?: string
131
+ }>): void
132
+ after(fn: AfterHook<Store, {
133
+ "title"?: string
134
+ "content"?: string
135
+ }, {
136
+ "id": string
137
+ "title": string
138
+ "content": string
139
+ "createdAt": string
140
+ "updatedAt": string
141
+ }>): void
142
+ }
143
+ "delete": {
144
+ before(fn: BeforeHook<Store, undefined>): void
145
+ after(fn: AfterHook<Store, undefined, {
146
+ "success": boolean
147
+ }>): void
148
+ }
149
+ }
150
+ "auth": {
151
+ "register": {
152
+ before(fn: BeforeHook<Store, {
153
+ "email": string
154
+ "password": string
155
+ }>): void
156
+ after(fn: AfterHook<Store, {
157
+ "email": string
158
+ "password": string
159
+ }, {
160
+ "email": string
161
+ "password": string
162
+ }>): void
163
+ }
164
+ "confirm": {
165
+ before(fn: BeforeHook<Store, {
166
+ "code": string
167
+ }>): void
168
+ after(fn: AfterHook<Store, {
169
+ "code": string
170
+ }, {
171
+ "code": string
172
+ }>): void
173
+ }
174
+ }
175
+ "admin": {
176
+ "clearDatabase": {
177
+ before(fn: BeforeHook<Store, undefined>): void
178
+ after(fn: AfterHook<Store, undefined, {
179
+ "success": boolean
180
+ }>): void
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Тип регистратора хуков для ЭТОГО бэкенда. Объявите хуки в отдельном
187
+ * файле и положите одной переменной в конфиг (`EbelyConfig.hooks`).
188
+ * Передайте СВОЙ класс store параметром, чтобы `ctx` был типизирован:
189
+ *
190
+ * import type { UserStore } from './userStore'
191
+ * export const hooks: Hooks<UserStore> = (h) => {
192
+ * h.posts.create.after(({ response, ctx }) => { … })
193
+ * }
194
+ *
195
+ * (Параметр НЕ выводится из `ebely.userStore` намеренно: это создало
196
+ * бы цикл типов `ebely` ⇄ `Hooks`, т.к. `hooks` лежит внутри `ebely`.)
197
+ */
198
+ export type Hooks<Store extends BaseStore = BaseStore> = (
199
+ h: EbelyHookTree<Store>,
200
+ ) => void
201
+
202
+ /**
203
+ * База `World` — это сконфигурированный `ebely.worldStore` (или пустой
204
+ * `BaseStore`, если не задан). Поэтому `world.<сценарий>()` и
205
+ * `world.get/set` доступны и типизированы ровно как у пользователя,
206
+ * только область — весь мир. Тип берётся из `ebely` тем же приёмом, что
207
+ * и `Store` (никаких рантайм-условий — см. ARCHITECTURE.md §8).
208
+ */
209
+ type ConfiguredWorldStore =
210
+ typeof ebely extends { worldStore: new () => infer I extends BaseStore }
211
+ ? I
212
+ : BaseStore
213
+ const WorldStoreBase = ((ebely as { worldStore?: new () => BaseStore })
214
+ .worldStore ?? BaseStore) as new () => ConfiguredWorldStore
215
+
216
+ export class World<
217
+ Store extends BaseStore = InstanceType<typeof ebely.userStore>,
218
+ > extends WorldStoreBase {
219
+ /**
220
+ * Общий реестр хуков. Регистрация — статическая (один раз из конфига),
221
+ * но `ctx` подставляется в момент запроса = store конкретного юзера.
222
+ */
223
+ private hookRegistry = new HookRegistry()
224
+
225
+ constructor(
226
+ public args: {
227
+ /** URL бэкенда. Если не задан — берётся ebely.url из конфига. */
228
+ url?: string
229
+ /** Класс-хранилище. Если не задан — берётся ebely.userStore. */
230
+ store?: new () => Store
231
+ } = {},
232
+ ) {
233
+ super()
234
+ const registrar = (ebely as { hooks?: (h: unknown) => void }).hooks
235
+ if (registrar) registrar(this.buildHookTree())
236
+ // world-store ходит АНОНИМНЫМ клиентом (без per-user заголовков);
237
+ // ctx хуков для таких вызовов = сам world.
238
+ ;(this as unknown as { api: unknown }).api = this.buildApiTree(
239
+ this.makeRequest({ headers: {}, store: this }),
240
+ )
241
+ }
242
+
243
+ /** Базовый URL с учётом server.url из схемы. */
244
+ private baseUrl(): string {
245
+ return (this.args.url ?? ebely.url).replace(/\/$/, '') + "/api"
246
+ }
247
+
248
+ private buildHookTree(): EbelyHookTree<Store> {
249
+ const r = this.hookRegistry
250
+ return {
251
+ "posts": {
252
+ "list": {
253
+ before: (fn: BeforeHook<Store>) => r.before({ key: "posts.list", fn }),
254
+ after: (fn: AfterHook<Store>) => r.after({ key: "posts.list", fn }),
255
+ },
256
+ "create": {
257
+ before: (fn: BeforeHook<Store>) => r.before({ key: "posts.create", fn }),
258
+ after: (fn: AfterHook<Store>) => r.after({ key: "posts.create", fn }),
259
+ },
260
+ "get": {
261
+ before: (fn: BeforeHook<Store>) => r.before({ key: "posts.get", fn }),
262
+ after: (fn: AfterHook<Store>) => r.after({ key: "posts.get", fn }),
263
+ },
264
+ "update": {
265
+ before: (fn: BeforeHook<Store>) => r.before({ key: "posts.update", fn }),
266
+ after: (fn: AfterHook<Store>) => r.after({ key: "posts.update", fn }),
267
+ },
268
+ "delete": {
269
+ before: (fn: BeforeHook<Store>) => r.before({ key: "posts.delete", fn }),
270
+ after: (fn: AfterHook<Store>) => r.after({ key: "posts.delete", fn }),
271
+ },
272
+ },
273
+ "auth": {
274
+ "register": {
275
+ before: (fn: BeforeHook<Store>) => r.before({ key: "auth.register", fn }),
276
+ after: (fn: AfterHook<Store>) => r.after({ key: "auth.register", fn }),
277
+ },
278
+ "confirm": {
279
+ before: (fn: BeforeHook<Store>) => r.before({ key: "auth.confirm", fn }),
280
+ after: (fn: AfterHook<Store>) => r.after({ key: "auth.confirm", fn }),
281
+ },
282
+ },
283
+ "admin": {
284
+ "clearDatabase": {
285
+ before: (fn: BeforeHook<Store>) => r.before({ key: "admin.clearDatabase", fn }),
286
+ after: (fn: AfterHook<Store>) => r.after({ key: "admin.clearDatabase", fn }),
287
+ },
288
+ },
289
+ } as unknown as EbelyHookTree<Store>
290
+ }
291
+
292
+ /**
293
+ * Создаёт `request`, замкнутый на заголовки и `store` (он же `ctx`
294
+ * хуков). Один и тот же движок и для пользователя, и для world.
295
+ */
296
+ private makeRequest(cfg: {
297
+ headers: Record<string, string>
298
+ store: BaseStore
299
+ }): RequestFn {
300
+ const baseUrl = this.baseUrl()
301
+ const registry = this.hookRegistry
302
+ const { headers: baseHeaders, store } = cfg
303
+
304
+ return async (req): Promise<{ status: number; body: unknown }> => {
305
+ const { method, path, opKey, input } = req
306
+
307
+ const hookReq = {
308
+ method,
309
+ path,
310
+ pathParams: { ...(input?.path ?? {}) },
311
+ query: { ...(input?.query ?? {}) },
312
+ body: input?.body,
313
+ headers: { ...baseHeaders },
314
+ }
315
+ await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
316
+
317
+ let resolvedPath = path
318
+ for (const [k, v] of Object.entries(hookReq.pathParams)) {
319
+ resolvedPath = resolvedPath.replace(
320
+ `{${k}}`,
321
+ encodeURIComponent(String(v)),
322
+ )
323
+ }
324
+
325
+ const url = new URL(baseUrl + resolvedPath)
326
+ for (const [k, v] of Object.entries(hookReq.query)) {
327
+ if (v !== undefined) url.searchParams.set(k, String(v))
328
+ }
329
+
330
+ const hasBody = hookReq.body !== undefined
331
+ const response = await fetch(url, {
332
+ method,
333
+ headers: {
334
+ ...(hasBody ? { 'content-type': 'application/json' } : {}),
335
+ ...hookReq.headers,
336
+ },
337
+ body: hasBody ? JSON.stringify(hookReq.body) : undefined,
338
+ })
339
+
340
+ const text = await response.text()
341
+ let data: unknown
342
+ try {
343
+ data = text ? JSON.parse(text) : undefined
344
+ } catch {
345
+ data = text
346
+ }
347
+
348
+ await registry.runAfter({
349
+ key: opKey,
350
+ request: hookReq,
351
+ response: { status: response.status, body: data },
352
+ ctx: store,
353
+ })
354
+
355
+ return { status: response.status, body: data }
356
+ }
357
+ }
358
+
359
+ /** Дерево типизированных вызовов эндпоинтов поверх одного `request`. */
360
+ private buildApiTree(request: RequestFn) {
361
+ return {
362
+ "posts": {
363
+
364
+ /** List all posts */
365
+ "list": async (input?: {}): Promise<ApiResponse<{ 200: Array<{
366
+ "id": string
367
+ "title": string
368
+ "content": string
369
+ "createdAt": string
370
+ "updatedAt": string
371
+ }> }>> => {
372
+ const res = await request({
373
+ method: "GET",
374
+ path: "/posts",
375
+ opKey: "posts.list",
376
+ input: input as RequestInput,
377
+ })
378
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: Array<{
379
+ "id": string
380
+ "title": string
381
+ "content": string
382
+ "createdAt": string
383
+ "updatedAt": string
384
+ }> }>
385
+ },
386
+
387
+ /** Create a post */
388
+ "create": async (input: { body: {
389
+ "title": string
390
+ "content": string
391
+ } }): Promise<ApiResponse<{ 200: {
392
+ "id": string
393
+ "title": string
394
+ "content": string
395
+ "createdAt": string
396
+ "updatedAt": string
397
+ } }>> => {
398
+ const res = await request({
399
+ method: "POST",
400
+ path: "/posts",
401
+ opKey: "posts.create",
402
+ input: input as RequestInput,
403
+ })
404
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
405
+ "id": string
406
+ "title": string
407
+ "content": string
408
+ "createdAt": string
409
+ "updatedAt": string
410
+ } }>
411
+ },
412
+
413
+ /** Get a single post by id */
414
+ "get": async (input: { path: { "id": string } }): Promise<ApiResponse<{ 200: {
415
+ "id": string
416
+ "title": string
417
+ "content": string
418
+ "createdAt": string
419
+ "updatedAt": string
420
+ } }>> => {
421
+ const res = await request({
422
+ method: "GET",
423
+ path: "/posts/{id}",
424
+ opKey: "posts.get",
425
+ input: input as RequestInput,
426
+ })
427
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
428
+ "id": string
429
+ "title": string
430
+ "content": string
431
+ "createdAt": string
432
+ "updatedAt": string
433
+ } }>
434
+ },
435
+
436
+ /** Update a post */
437
+ "update": async (input: { path: { "id": string }; body?: {
438
+ "title"?: string
439
+ "content"?: string
440
+ } }): Promise<ApiResponse<{ 200: {
441
+ "id": string
442
+ "title": string
443
+ "content": string
444
+ "createdAt": string
445
+ "updatedAt": string
446
+ } }>> => {
447
+ const res = await request({
448
+ method: "PATCH",
449
+ path: "/posts/{id}",
450
+ opKey: "posts.update",
451
+ input: input as RequestInput,
452
+ })
453
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
454
+ "id": string
455
+ "title": string
456
+ "content": string
457
+ "createdAt": string
458
+ "updatedAt": string
459
+ } }>
460
+ },
461
+
462
+ /** Delete a post */
463
+ "delete": async (input: { path: { "id": string } }): Promise<ApiResponse<{ 200: {
464
+ "success": boolean
465
+ } }>> => {
466
+ const res = await request({
467
+ method: "DELETE",
468
+ path: "/posts/{id}",
469
+ opKey: "posts.delete",
470
+ input: input as RequestInput,
471
+ })
472
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
473
+ "success": boolean
474
+ } }>
475
+ },
476
+ },
477
+ "auth": {
478
+
479
+ /** Register (stub: echoes input) */
480
+ "register": async (input: { body: {
481
+ "email": string
482
+ "password": string
483
+ } }): Promise<ApiResponse<{ 200: {
484
+ "email": string
485
+ "password": string
486
+ } }>> => {
487
+ const res = await request({
488
+ method: "POST",
489
+ path: "/auth/register",
490
+ opKey: "auth.register",
491
+ input: input as RequestInput,
492
+ })
493
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
494
+ "email": string
495
+ "password": string
496
+ } }>
497
+ },
498
+
499
+ /** Confirm registration code (stub: echoes input) */
500
+ "confirm": async (input: { body: {
501
+ "code": string
502
+ } }): Promise<ApiResponse<{ 200: {
503
+ "code": string
504
+ } }>> => {
505
+ const res = await request({
506
+ method: "POST",
507
+ path: "/auth/confirm",
508
+ opKey: "auth.confirm",
509
+ input: input as RequestInput,
510
+ })
511
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
512
+ "code": string
513
+ } }>
514
+ },
515
+ },
516
+ "admin": {
517
+
518
+ /** Wipe all in-memory data */
519
+ "clearDatabase": async (input?: {}): Promise<ApiResponse<{ 200: {
520
+ "success": boolean
521
+ } }>> => {
522
+ const res = await request({
523
+ method: "POST",
524
+ path: "/admin/clear-database",
525
+ opKey: "admin.clearDatabase",
526
+ input: input as RequestInput,
527
+ })
528
+ return new ApiResponse(res) as unknown as ApiResponse<{ 200: {
529
+ "success": boolean
530
+ } }>
531
+ },
532
+ },
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Создаёт «пользователя» — изолированный набор типизированных вызовов
538
+ * эндпоинтов, который под капотом ходит fetch-запросами. `store.api`
539
+ * указывает на то же дерево, поэтому методы-сценарии этого store
540
+ * (`fullRegister` и т.п.) ходят от лица именно этого пользователя.
541
+ */
542
+ createUser(userArgs: CreateUserArgs = {}) {
543
+ const StoreClass =
544
+ this.args.store ?? (ebely.userStore as unknown as new () => Store)
545
+ const store = new StoreClass()
546
+ const tree = this.buildApiTree(
547
+ this.makeRequest({ headers: { ...userArgs.headers }, store }),
548
+ )
549
+ ;(store as unknown as { api: unknown }).api = tree
550
+ return Object.assign(store, tree)
551
+ }
552
+ }
@@ -0,0 +1,19 @@
1
+ // Переиспользуемые именованные хендлеры. Типизируются библиотечными
2
+ // дженериками `BeforeHook` / `AfterHook` (привязаны к твоему `UserStore`),
3
+ // поэтому один хендлер можно вешать на разные endpoint'ы из `hooks.ts`.
4
+
5
+ import type { AfterHook, BeforeHook } from 'ebely'
6
+ import type { UserStore } from './userStore'
7
+
8
+ /** Логирует любой ответ: метод, путь, статус, тело. */
9
+ export const logResponse: AfterHook<UserStore> = ({ request, response }) => {
10
+ console.log(
11
+ `[${request.method}] ${request.path} → ${response.status}`,
12
+ response.body,
13
+ )
14
+ }
15
+
16
+ /** Подставляет общий заголовок трассировки в любой запрос. */
17
+ export const withTrace: BeforeHook<UserStore> = ({ request }) => {
18
+ request.headers['x-trace'] = 'demo'
19
+ }
@@ -0,0 +1,28 @@
1
+ // Хуки before/after для этого бэкенда. Файл ПОЛНОСТЬЮ типизирован: тип
2
+ // `Hooks` экспортирует сгенерированный клиент (`generated.ts`), поэтому
3
+ // `h.posts.create` автокомплитится, `response.body.id` типизирован, а
4
+ // `ctx` — это твой `UserStore`.
5
+ //
6
+ // Регистрировать можно где угодно (это просто функция) — кладётся одной
7
+ // переменной в конфиг (`ebely.ts`). Вызывается один раз при `new World()`.
8
+ // Важно: `ctx` внутри хука — store КОНКРЕТНОГО пользователя, сделавшего
9
+ // запрос, поэтому запись в переменные не «течёт» между пользователями.
10
+
11
+ import type { Hooks } from './generated'
12
+ import type { UserStore } from './userStore'
13
+ import { logResponse } from './handlers'
14
+
15
+ export const hooks: Hooks<UserStore> = (h) => {
16
+ // после создания поста — сохранить id в переменные ИМЕННО этого юзера
17
+ h.posts.create.after(({ response, ctx }) => {
18
+ ctx.set({ key: 'lastPostId', value: response.body.id })
19
+ })
20
+
21
+ // перед получением поста — подставить заголовок (before может править запрос)
22
+ h.posts.get.before(({ request }) => {
23
+ request.headers['x-trace'] = 'demo'
24
+ })
25
+
26
+ // несколько хуков на один endpoint складываются в очередь
27
+ h.posts.create.after(logResponse)
28
+ }
@@ -0,0 +1,56 @@
1
+ // Внутренние переменные и СЦЕНАРИИ одного пользователя.
2
+ //
3
+ // Второй дженерик `BaseStore<Vars, WorldApi>` даёт `this.api` —
4
+ // типизированное дерево вызовов эндпоинтов ОТ ЛИЦА ЭТОГО пользователя
5
+ // (его заголовки, его переменные, его `ctx` в хуках). Поэтому
6
+ // многошаговую подготовку (регистрация → подтверждение по коду) можно
7
+ // спрятать за одним методом `fullRegister`, а в тестах звать его одной
8
+ // строкой. `WorldApi` — `import type` → рантайм-цикла нет (как hooks.ts).
9
+
10
+ import { BaseStore } from "ebely"
11
+ import type { WorldApi } from "./generated"
12
+
13
+ export type InternalVariable = {
14
+ email: string
15
+ password: string
16
+ accessToken: string
17
+ deviceId: string
18
+ telegramUsername: string
19
+ createdPosts: { postId: string; date: string }[]
20
+ lastPostId: string
21
+ }
22
+
23
+ export class UserStore extends BaseStore<InternalVariable, WorldApi> {
24
+ /** Пример производного метода: id первого созданного поста. */
25
+ public getFirstPostId(): string | undefined {
26
+ const createdPosts = this.get({ key: "createdPosts" })
27
+ return createdPosts?.[0]?.postId
28
+ }
29
+
30
+ /**
31
+ * Сценарий: полная регистрация одним вызовом. Под капотом — серия
32
+ * эндпоинтов (register → confirm), как было бы в реальном флоу с
33
+ * кодом из письма. Тест зовёт это одной строкой и не дублирует шаги.
34
+ */
35
+ public async fullRegister(args: {
36
+ email: string
37
+ password: string
38
+ }): Promise<void> {
39
+ const { email, password } = args
40
+
41
+ const registered = await this.api.auth.register({
42
+ body: { email, password },
43
+ })
44
+ registered.assert(200, { email })
45
+
46
+ // «код из письма» — здесь просто заглушка (бэкенд его не проверяет)
47
+ const confirmed = await this.api.auth.confirm({
48
+ body: { code: "0000" },
49
+ })
50
+ confirmed.assert(200)
51
+
52
+ this.set({ key: "email", value: email })
53
+ this.set({ key: "password", value: password })
54
+ this.set({ key: "accessToken", value: `token-for-${email}` })
55
+ }
56
+ }
@@ -0,0 +1,26 @@
1
+ // Внутренние переменные и СЦЕНАРИИ уровня WORLD — то же, что
2
+ // `userStore.ts`, но область не «один пользователь», а весь мир.
3
+ // Экземпляр этого класса и есть `world` (генерируемый `World` наследует
4
+ // его), поэтому всё объявленное здесь доступно как `world.<...>()`.
5
+ //
6
+ // `this.api` здесь — АНОНИМНЫЙ клиент (без per-user заголовков): сценарии
7
+ // подготовки/очистки бьют по бэкенду «от имени мира». `WorldApi`
8
+ // импортируется как `import type` → рантайм-цикла нет (см. hooks.ts /
9
+ // ARCHITECTURE.md §8).
10
+
11
+ import { BaseStore } from "ebely"
12
+ import type { WorldApi } from "./generated"
13
+
14
+ export type WorldVariable = {
15
+ /** Сколько раз за прогон чистили базу — пример world-переменной. */
16
+ resets: number
17
+ }
18
+
19
+ export class WorldStore extends BaseStore<WorldVariable, WorldApi> {
20
+ /** Сценарий: очистить базу одним вызовом `world.clearDatabase()`. */
21
+ public async clearDatabase(): Promise<void> {
22
+ const res = await this.api.admin.clearDatabase()
23
+ res.assert(200, { success: true })
24
+ this.set({ key: "resets", value: (this.get({ key: "resets" }) ?? 0) + 1 })
25
+ }
26
+ }